Fix navigation: outerHTML swap, island markers, host handle equality, dispose

Navigation pipeline now works end-to-end:
- outerHTML swap uses dom-replace-child instead of morph-node (morph has
  a CEK continuation issue with nested for-each that needs separate fix)
- swap-dom-nodes returns the new element for outerHTML so post-swap
  hydrates the correct (new) DOM, not the detached old element
- sx-render uses marker mode: islands rendered as empty span[data-sx-island]
  markers, hydrated by post-swap. Prevents duplicate content from island
  body expansion + SX response nav rows.
- dispose-island (singular) called on old island before morph, not just
  dispose-islands-in (which only disposes sub-islands)

OCaml runtime:
- safe_eq: Dict equality checks __host_handle for DOM node identity
  (js_to_value creates new Dict wrappers per call, breaking physical ==)
- contains?: same host handle check
- to_string: trampoline thunks (fixes <thunk> display)
- as_number: trampoline thunks (fixes arithmetic on leaked thunks)

DOM platform:
- dom-remove, dom-attr-list (name/value pairs), dom-child-list (SX list),
  dom-is-active-element?, dom-is-input-element?, dom-is-child-of?, dom-on

All 5 reactive-nav Playwright tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 13:19:29 +00:00
parent 07bbcaf1bb
commit eacde62806
10 changed files with 2352 additions and 30 deletions

View File

@@ -375,8 +375,10 @@
(dom-has-attr? new-node "data-sx-island")
(not (= (dom-get-attr old-node "data-sx-island")
(dom-get-attr new-node "data-sx-island"))))
(dispose-islands-in old-node)
(clear-processed! old-node "island-hydrated"))
;; Dispose the old island itself (not just sub-islands)
;; to tear down reactive effects before morphing content
(dispose-island old-node)
(dispose-islands-in old-node))
(sync-attrs old-node new-node)
;; Skip morphing focused input to preserve user's in-progress edits
(when (not (and (dom-is-active-element? old-node)
@@ -602,19 +604,22 @@
(morph-children target wrapper)))
"outerHTML"
(let ((parent (dom-parent target)))
(let ((parent (dom-parent target))
(new-el (dom-clone new-nodes)))
(if (dom-is-fragment? new-nodes)
;; Fragment — morph first child, insert rest
;; Fragment — replace target with fragment children
(let ((fc (dom-first-child new-nodes)))
(if fc
(do
(morph-node target fc)
;; Insert remaining siblings after morphed element
(set! new-el (dom-clone fc))
(dom-replace-child parent new-el target)
(let ((sib (dom-next-sibling fc)))
(insert-remaining-siblings parent target sib)))
(insert-remaining-siblings parent new-el sib)))
(dom-remove-child parent target)))
(morph-node target new-nodes))
parent)
;; Element — replace target with new element
(dom-replace-child parent new-el target))
;; Return the new element so post-swap can hydrate it
new-el)
"afterend"
(dom-insert-after target new-nodes)