Commit330f76d6Recorded5 Jul 2026Repositorysigil-web

Fix ui-update morph: morph the element itself (outerHTML), not innerHTML

Message

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).

Changed
 assets/sigil-web/js/sigil-web-ui.js | 24 ++++++++++++++++++++++--
 src/sigil/web/ui.sgl                | 12 ++++++++----
 2 files changed, 30 insertions(+), 6 deletions(-)
Diff
assets/sigil-web/js/sigil-web-ui.jsmodified
@@ -140,6 +140,22 @@
140
141
switch (mode) {
142
case 'morph':
+143
// The content is a full element identified by a matching id, so morph
+144
// the TARGET ELEMENT ITSELF (idiomorph's default outerHTML style).
+145
// Morphing as innerHTML would nest the element inside the target
+146
// (a reset <form> inside #add-form => invalid nested form => broken
+147
// submit => full page reload). This is the ui-update default.
+148
if (typeof Idiomorph !== 'undefined') {
+149
Idiomorph.morph(el, html);
+150
} else {
+151
console.warn('[SigilUI] Idiomorph not loaded, falling back to outerHTML');
+152
el.outerHTML = html;
+153
}
+154
break;
+155
case 'morph-inner':
+156
// Morph the target's INNER content (idiomorph innerHTML). The content
+157
// is the new children of the target, not a replacement element. Used
+158
// by the deprecated header path (sigil-ui-response).
159
if (typeof Idiomorph !== 'undefined') {
160
Idiomorph.morph(el, html, { morphStyle: 'innerHTML' });
161
} else {
@@ -414,9 +430,13 @@
430
431
// Legacy single-fragment path (deprecated sigil-ui-response): an HTML
432
// body morphed into the target named by the Sigil-UI-Merge-* headers
417
// (falling back to the triggering element's data-sg-target).
+433
// (falling back to the triggering element's data-sg-target). This path
+434
// has always morphed the target's INNER content, so its default "morph"
+435
// maps to "morph-inner" to preserve that behavior now that the plain
+436
// "morph" mode morphs the element itself.
437
const mergeTarget = response.headers.get('Sigil-UI-Merge-Target') || target;
419
const mergeMode = response.headers.get('Sigil-UI-Merge-Mode') || mode;
+438
const rawMode = response.headers.get('Sigil-UI-Merge-Mode') || mode;
+439
const mergeMode = (rawMode === 'morph') ? 'morph-inner' : rawMode;
440
const html = await response.text();
441
if (mergeTarget) {
442
this.handleMorph({ target: mergeTarget, html, mode: mergeMode });
src/sigil/web/ui.sglmodified
@@ -182,10 +182,14 @@
182
;;; settle: Milliseconds to wait after applying (for CSS transitions)
183
;;;
184
;;; Modes:
185
;;; "morph" - Intelligent diff/patch via idiomorph (default)
186
;;; "replace" - Replace target's outerHTML
187
;;; "inner" - Replace target's innerHTML
188
;;; "append" - Append to target's children
+185
;;; "morph" - Morph the target ELEMENT itself via idiomorph, in
+186
;;; place (default). The content is a full element whose
+187
;;; id matches the target — the common case.
+188
;;; "morph-inner" - Morph the target's INNER content via idiomorph (the
+189
;;; content is the target's new children).
+190
;;; "replace" - Replace target's outerHTML (no diffing)
+191
;;; "inner" - Replace target's innerHTML (no diffing)
+192
;;; "append" - Append to target's children
193
;;; "prepend" - Prepend to target's children
194
;;; "before" - Insert before target
195
;;; "after" - Insert after target