fix(state): 隔离嵌套控件绑定事件(by AI)

This commit is contained in:
Star 2026-08-15 22:51:01 +08:00
parent fa76660e75
commit d93c6463ca
5 changed files with 14 additions and 1 deletions

View File

@ -9,6 +9,9 @@
### 安全性
- **异步隔离**: 翻译请求按语言和原文去重,丢弃旧语言返回值,并隔离 translator 的同步异常。
### 修复
- **组件绑定事件**: 容器或自定义组件的 `$bind` 不再消费嵌套表单控件冒泡的 `change` 事件,避免父级状态被子控件值覆盖。
## v1.0.26 (2026-08-14)
### 修复

1
dist/state.js vendored
View File

@ -608,6 +608,7 @@
if (realAttrName === "bind") {
const isTextInput = ["INPUT", "TEXTAREA"].includes(node.tagName) && ["textarea", "text", "password", "email", "number", "search", "url", "tel"].includes(node.type || "text") || node.isContentEditable;
node.addEventListener(isTextInput ? "input" : "change", (e) => {
if (e.target !== node) return;
let newVal = node.isContentEditable ? e.target.innerHTML : node.type === "checkbox" ? e.target.checked : node.type === "file" ? e.target.files : e.target.value ?? e.detail;
_setNoWriteBack(node);
setDisableRunCodeError(true);

2
dist/state.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -421,6 +421,7 @@ export function _parseNode(node, scanObj) {
if (realAttrName === 'bind') {
const isTextInput = (['INPUT', 'TEXTAREA'].includes(node.tagName) && ['textarea', 'text', 'password', 'email', 'number', 'search', 'url', 'tel'].includes(node.type || 'text')) || node.isContentEditable;
node.addEventListener(isTextInput ? 'input' : 'change', (e) => {
if (e.target !== node) return;
let newVal = node.isContentEditable ? e.target.innerHTML : (node.type === 'checkbox' ? e.target.checked : (node.type === 'file' ? e.target.files : (e.target.value ?? e.detail)));
_setNoWriteBack(node); setDisableRunCodeError(true);
if (node.type === 'checkbox' && node._checkboxMultiMode) _runCode(`!!checked ? (!${tpl}.includes(val) && ${tpl}.push(val)) : (index = ${tpl}.indexOf(val), index > -1 && ${tpl}.splice(index, 1))`, { val: node.value, checked: newVal, thisNode: node }, scanObj.thisObj || node, node._ref || {});

View File

@ -84,6 +84,14 @@ window.testDom = async function() {
input.dispatchEvent(new Event('input'));
if (state.val !== '') throw new Error('$bind must preserve an empty text value');
// A component/container binding must not consume change events bubbling
// from nested form controls.
document.body.innerHTML = '<div id="test-parent-bind" $bind="state.parentValue"><input id="test-nested-change"></div>';
state.parentValue = 'kept';
__unsafeRefreshState(document.documentElement);
$('#test-nested-change').dispatchEvent(new Event('change', { bubbles: true }));
if (state.parentValue !== 'kept') throw new Error('$bind parent consumed a nested control change event');
// 6. A select bound before its template-rendered options must retain the
// model value instead of writing the browser-selected default back.
document.body.innerHTML = '<select id="test-select" $bind="state.selected"><template $each="state.options"><option $text="item" $value="item"></option></template></select>';