Fix ui-update morph: morph the element itself (outerHTML), not innerHTML
Regression from the response unification. handleMorph mode:"morph" applied idiomorph with morphStyle:'innerHTML', but a ui-update's content is a FULL element identified by a matching id. Applied as innerHTML it nested the element inside the target: a reset <form> inside #add-form => invalid nested form => the inner form's submit hook breaks => native navigation => full page reload that wiped the ephemeral log. (Rows had the same nesting but <li> tolerated it.)
Fix: mode:"morph" now morphs the TARGET ELEMENT ITSELF via idiomorph's default (outerHTML). Added mode:"morph-inner" for the old inner-content idiomorph, and the deprecated header path (sigil-ui-response) maps its default "morph" to "morph-inner" so its behavior is unchanged (the demo sends inner content to #records-list and must keep the target's id).
Verified via headless Chrome + CDP against the running app: - sequential add x3: no page reload (sentinel survived), 3 items, 3 log lines persist, count '3 items', input reset, zero nested forms; - row toggle morph: class applied in place, zero nested <li>; - morph-inner: target keeps its id, inner content updated (demo preserved).
assets/sigil-web/js/sigil-web-ui.js | 24 ++++++++++++++++++++++--
src/sigil/web/ui.sgl | 12 ++++++++----
2 files changed, 30 insertions(+), 6 deletions(-)assets/sigil-web/js/sigil-web-ui.jsmodified
switch (mode) { case 'morph': // The content is a full element identified by a matching id, so morph // the TARGET ELEMENT ITSELF (idiomorph's default outerHTML style). // Morphing as innerHTML would nest the element inside the target // (a reset <form> inside #add-form => invalid nested form => broken // submit => full page reload). This is the ui-update default. if (typeof Idiomorph !== 'undefined') { Idiomorph.morph(el, html); } else { console.warn('[SigilUI] Idiomorph not loaded, falling back to outerHTML'); el.outerHTML = html; } break; case 'morph-inner': // Morph the target's INNER content (idiomorph innerHTML). The content // is the new children of the target, not a replacement element. Used // by the deprecated header path (sigil-ui-response). if (typeof Idiomorph !== 'undefined') { Idiomorph.morph(el, html, { morphStyle: 'innerHTML' }); } else { // Legacy single-fragment path (deprecated sigil-ui-response): an HTML // body morphed into the target named by the Sigil-UI-Merge-* headers // (falling back to the triggering element's data-sg-target). // (falling back to the triggering element's data-sg-target). This path // has always morphed the target's INNER content, so its default "morph" // maps to "morph-inner" to preserve that behavior now that the plain // "morph" mode morphs the element itself. const mergeTarget = response.headers.get('Sigil-UI-Merge-Target') || target; const mergeMode = response.headers.get('Sigil-UI-Merge-Mode') || mode; const rawMode = response.headers.get('Sigil-UI-Merge-Mode') || mode; const mergeMode = (rawMode === 'morph') ? 'morph-inner' : rawMode; const html = await response.text(); if (mergeTarget) { this.handleMorph({ target: mergeTarget, html, mode: mergeMode });src/sigil/web/ui.sglmodified
;;; settle: Milliseconds to wait after applying (for CSS transitions) ;;; ;;; Modes: ;;; "morph" - Intelligent diff/patch via idiomorph (default) ;;; "replace" - Replace target's outerHTML ;;; "inner" - Replace target's innerHTML ;;; "append" - Append to target's children ;;; "morph" - Morph the target ELEMENT itself via idiomorph, in ;;; place (default). The content is a full element whose ;;; id matches the target — the common case. ;;; "morph-inner" - Morph the target's INNER content via idiomorph (the ;;; content is the target's new children). ;;; "replace" - Replace target's outerHTML (no diffing) ;;; "inner" - Replace target's innerHTML (no diffing) ;;; "append" - Append to target's children ;;; "prepend" - Prepend to target's children ;;; "before" - Insert before target ;;; "after" - Insert after target