Fix morph-children: empty-id keying, consumed-set cleanup, island attr sync

Three bugs in the DOM morph algorithm (web/engine.sx):

1. Empty-id keying: dom-id returns "" (not nil) for elements without an
   id attribute, and "" is truthy in SX. Every id-less element was stored
   under key "" in old-by-id, causing all new children to match the same
   old element via the keyed branch — collapsing all children into one.
   Fix: guard with (and id (not (empty? id))) in map building and matching.

2. Cleanup bug: the oi-cursor cleanup (range oi len) removed keyed elements
   that were matched and moved from positions >= oi, and failed to remove
   unmatched elements at positions < oi. Fix: track consumed indices in a
   dict and remove all unconsumed elements regardless of position.

3. Island attr sync: morph-node delegated to morph-island-children without
   first syncing the island element's own attributes (e.g. data-sx-state).
   Fix: call sync-attrs before morph-island-children.

Also: pass explicit `true` to all dom-clone calls (deep clone parameter).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 23:58:23 +00:00
parent d81a518732
commit 5abc947ac7
2 changed files with 50 additions and 28 deletions

File diff suppressed because one or more lines are too long

View File

@@ -342,11 +342,16 @@
(=
(dom-get-attr old-node "data-sx-island")
(dom-get-attr new-node "data-sx-island")))
(morph-island-children old-node new-node)
(do
(sync-attrs old-node new-node)
(morph-island-children old-node new-node))
(or
(not (= (dom-node-type old-node) (dom-node-type new-node)))
(not (= (dom-node-name old-node) (dom-node-name new-node))))
(dom-replace-child (dom-parent old-node) (dom-clone new-node) old-node)
(dom-replace-child
(dom-parent old-node)
(dom-clone new-node true)
old-node)
(or (= (dom-node-type old-node) 3) (= (dom-node-type old-node) 8))
(when
(not (= (dom-text-content old-node) (dom-text-content new-node)))
@@ -411,25 +416,37 @@
(let
((old-kids (dom-child-list old-parent))
(new-kids (dom-child-list new-parent))
(old-by-id
(reduce
(fn
((acc :as dict) kid)
(let
((id (dom-id kid)))
(if id (do (dict-set! acc id kid) acc) acc)))
(dict)
old-kids))
(oi 0))
(old-by-id (dict))
(old-idx-by-id (dict))
(consumed (dict))
(oi 0)
(idx 0))
(for-each
(fn
(kid)
(let
((id (dom-id kid)))
(when
(and id (not (empty? id)))
(dict-set! old-by-id id kid)
(dict-set! old-idx-by-id id idx)))
(set! idx (inc idx)))
old-kids)
(for-each
(fn
(new-child)
(let
((match-id (dom-id new-child))
((raw-id (dom-id new-child))
(match-id (if (and raw-id (not (empty? raw-id))) raw-id nil))
(match-by-id (if match-id (dict-get old-by-id match-id) nil)))
(cond
(and match-by-id (not (nil? match-by-id)))
(do
(let
((matched-idx (dict-get old-idx-by-id match-id)))
(when
matched-idx
(dict-set! consumed (str matched-idx) true)))
(when
(and
(< oi (len old-kids))
@@ -443,20 +460,25 @@
(< oi (len old-kids))
(let
((old-child (nth old-kids oi)))
(if
(and (dom-id old-child) (not match-id))
(dom-insert-before
old-parent
(dom-clone new-child)
old-child)
(do (morph-node old-child new-child) (set! oi (inc oi)))))
:else (dom-append old-parent (dom-clone new-child)))))
(let
((old-id (dom-id old-child)))
(if
(and old-id (not (empty? old-id)) (not match-id))
(dom-insert-before
old-parent
(dom-clone new-child true)
old-child)
(do
(dict-set! consumed (str oi) true)
(morph-node old-child new-child)
(set! oi (inc oi))))))
:else (dom-append old-parent (dom-clone new-child true)))))
new-kids)
(for-each
(fn
((i :as number))
(i)
(when
(>= i oi)
(not (dict-get consumed (str i)))
(let
((leftover (nth old-kids i)))
(when
@@ -465,7 +487,7 @@
(not (dom-has-attr? leftover "sx-preserve"))
(not (dom-has-attr? leftover "sx-ignore")))
(dom-remove-child old-parent leftover)))))
(range oi (len old-kids))))))
(range 0 (len old-kids))))))
(define
morph-island-children
@@ -588,7 +610,7 @@
(morph-children target wrapper)))
"outerHTML"
(let
((parent (dom-parent target)) (new-el (dom-clone new-nodes)))
((parent (dom-parent target)) (new-el (dom-clone new-nodes true)))
(if
(dom-is-fragment? new-nodes)
(let
@@ -596,7 +618,7 @@
(if
fc
(do
(set! new-el (dom-clone fc))
(set! new-el (dom-clone fc true))
(dom-replace-child parent new-el target)
(let
((sib (dom-next-sibling fc)))