修复(state): 修正条件组件重挂载与事件参数(by AI)

This commit is contained in:
Star 2026-08-14 15:55:58 +08:00
parent 2c0ffe61de
commit fd6a4c4e23
5 changed files with 52 additions and 14 deletions

View File

@ -1,5 +1,11 @@
# CHANGELOG # CHANGELOG
## v1.0.26 (2026-08-14)
### 修复
- **条件分支组件生命周期**: `$if` 分支重新显示时创建全新节点,确保已卸载组件会重新初始化,修复编辑器在切换后偶发空白的问题。
- **自定义事件参数兼容**: 原始类型的 `CustomEvent.detail` 不再被展开为事件参数,仍可通过 `event.detail` 正常读取。
## v1.0.25 (2026-07-13) ## v1.0.25 (2026-07-13)
### 修复 ### 修复

17
dist/state.js vendored
View File

@ -370,12 +370,14 @@
if (attr === "if") { if (attr === "if") {
if (result) { if (result) {
if (!node._renderedNodes || node._renderedNodes.length === 0) { if (!node._renderedNodes || node._renderedNodes.length === 0) {
node._children.forEach((child) => { const rendered = node._children.map((child) => {
node.parentNode.insertBefore(child, node); const cloned = child.cloneNode(true);
child._ref = { ...node._ref }; node.parentNode.insertBefore(cloned, node);
child._thisObj = node._thisObj; cloned._ref = { ...node._ref };
cloned._thisObj = node._thisObj;
return cloned;
}); });
node._renderedNodes = [node._children]; node._renderedNodes = [rendered];
} }
} else { } else {
_clearRenderedNodes(node); _clearRenderedNodes(node);
@ -545,7 +547,10 @@
if (eventName === "update") node._hasOnUpdate = true; if (eventName === "update") node._hasOnUpdate = true;
if (eventName === "load" && !["BODY", "IMG", "IFRAME"].includes(node.tagName)) node._hasOnLoad = true; if (eventName === "load" && !["BODY", "IMG", "IFRAME"].includes(node.tagName)) node._hasOnLoad = true;
if (eventName === "unload" && !["BODY", "IMG", "IFRAME"].includes(node.tagName)) node._hasOnUnload = true; if (eventName === "unload" && !["BODY", "IMG", "IFRAME"].includes(node.tagName)) node._hasOnUnload = true;
node.addEventListener(eventName, (e) => _runCode(tpl, { event: e, thisNode: node, ...e.detail || {} }, scanObj.thisObj || node, node._ref || {})); node.addEventListener(eventName, (e) => {
const detailVars = e.detail && typeof e.detail === "object" && !Array.isArray(e.detail) ? e.detail : {};
_runCode(tpl, { event: e, thisNode: node, ...detailVars }, scanObj.thisObj || node, node._ref || {});
});
} else { } else {
if (realAttrName === "bind") { if (realAttrName === "bind") {
const isTextInput = ["INPUT", "TEXTAREA"].includes(node.tagName) && ["textarea", "text", "password", "email", "number", "search", "url", "tel"].includes(node.type || "text") || node.isContentEditable; const isTextInput = ["INPUT", "TEXTAREA"].includes(node.tagName) && ["textarea", "text", "password", "email", "number", "search", "url", "tel"].includes(node.type || "text") || node.isContentEditable;

2
dist/state.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -202,12 +202,14 @@ export function _updateBinding(binding) {
if (attr === 'if') { if (attr === 'if') {
if (result) { if (result) {
if (!node._renderedNodes || node._renderedNodes.length === 0) { if (!node._renderedNodes || node._renderedNodes.length === 0) {
node._children.forEach(child => { const rendered = node._children.map(child => {
node.parentNode.insertBefore(child, node); const cloned = child.cloneNode(true);
child._ref = { ...node._ref }; node.parentNode.insertBefore(cloned, node);
child._thisObj = node._thisObj; cloned._ref = { ...node._ref };
cloned._thisObj = node._thisObj;
return cloned;
}); });
node._renderedNodes = [node._children]; node._renderedNodes = [rendered];
} }
} else { } else {
_clearRenderedNodes(node); _clearRenderedNodes(node);
@ -382,7 +384,10 @@ export function _parseNode(node, scanObj) {
if (eventName === 'update') node._hasOnUpdate = true; if (eventName === 'update') node._hasOnUpdate = true;
if (eventName === 'load' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnLoad = true; if (eventName === 'load' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnLoad = true;
if (eventName === 'unload' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnUnload = true; if (eventName === 'unload' && !['BODY', 'IMG', 'IFRAME'].includes(node.tagName)) node._hasOnUnload = true;
node.addEventListener(eventName, (e) => _runCode(tpl, { event: e, thisNode: node, ...(e.detail || {}) }, scanObj.thisObj || node, node._ref || {})); node.addEventListener(eventName, (e) => {
const detailVars = e.detail && typeof e.detail === 'object' && !Array.isArray(e.detail) ? e.detail : {};
_runCode(tpl, { event: e, thisNode: node, ...detailVars }, scanObj.thisObj || node, node._ref || {});
});
} else { } else {
if (realAttrName === 'bind') { if (realAttrName === 'bind') {
const isTextInput = (['INPUT', 'TEXTAREA'].includes(node.tagName) && ['textarea', 'text', 'password', 'email', 'number', 'search', 'url', 'tel'].includes(node.type || 'text')) || node.isContentEditable; const isTextInput = (['INPUT', 'TEXTAREA'].includes(node.tagName) && ['textarea', 'text', 'password', 'email', 'number', 'search', 'url', 'tel'].includes(node.type || 'text')) || node.isContentEditable;

View File

@ -1,6 +1,6 @@
// test/dom.test.js // test/dom.test.js
window.testDom = async function() { window.testDom = async function() {
const { __unsafeRefreshState, $, $$, NewState } = ApigoState; const { __unsafeRefreshState, $, $$, NewState, Component } = ApigoState;
console.log('Testing dom.js...'); console.log('Testing dom.js...');
const wait = () => new Promise(r => setTimeout(r, 10)); const wait = () => new Promise(r => setTimeout(r, 10));
@ -27,6 +27,21 @@ window.testDom = async function() {
await wait(); await wait();
if (!$('#test-if')) throw new Error('$if fail: should be visible'); if (!$('#test-if')) throw new Error('$if fail: should be visible');
// A component in a conditional branch must be initialized again after the
// branch is removed and mounted a second time.
let conditionalSetupCount = 0;
Component.register('IfLifecycleProbe', node => { conditionalSetupCount++; node.probeReady = true });
document.body.innerHTML = '<template $if="state.showComponent"><IfLifecycleProbe id="if-lifecycle-probe"></IfLifecycleProbe></template>';
state.showComponent = true;
__unsafeRefreshState(document.documentElement);
await wait();
if (!$('#if-lifecycle-probe')?.probeReady || conditionalSetupCount !== 1) throw new Error('$if component initial mount failed');
state.showComponent = false;
await wait();
state.showComponent = true;
await wait();
if (!$('#if-lifecycle-probe')?.probeReady || conditionalSetupCount !== 2) throw new Error('$if component remount must reinitialize a fresh node');
// 3. $each directive // 3. $each directive
document.body.innerHTML = '<template $each="state.items"><div class="test-item" $text="item"></div></template>'; document.body.innerHTML = '<template $each="state.items"><div class="test-item" $text="item"></div></template>';
state.items = ['A', 'B']; state.items = ['A', 'B'];
@ -46,6 +61,13 @@ window.testDom = async function() {
$('#test-click').click(); $('#test-click').click();
if (state.count !== 1) throw new Error('$onclick failed'); if (state.count !== 1) throw new Error('$onclick failed');
// Primitive CustomEvent details remain available through event.detail and
// must not be expanded into invalid numeric function argument names.
document.body.innerHTML = '<div id="test-primitive-event" $onchange="state.eventValue=event.detail"></div>';
__unsafeRefreshState(document.documentElement);
$('#test-primitive-event').dispatchEvent(new CustomEvent('change', { detail: 'source text' }));
if (state.eventValue !== 'source text') throw new Error('$on event must accept primitive detail');
// 5. $bind (input) // 5. $bind (input)
document.body.innerHTML = '<input id="test-bind" $bind="state.val">'; document.body.innerHTML = '<input id="test-bind" $bind="state.val">';
state.val = 'init'; state.val = 'init';