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:
@@ -151,7 +151,14 @@
|
||||
(and (starts-with? name "~")
|
||||
(env-has? env name)
|
||||
(island? (env-get env name)))
|
||||
(render-dom-island (env-get env name) args env ns)
|
||||
;; In marker mode (sx-render for swap), create empty marker span
|
||||
;; that post-swap will hydrate. Otherwise, full render.
|
||||
(if (scope-peek "sx-render-markers")
|
||||
(let ((island (env-get env name))
|
||||
(marker (dom-create-element "span" nil)))
|
||||
(dom-set-attr marker "data-sx-island" (component-name island))
|
||||
marker)
|
||||
(render-dom-island (env-get env name) args env ns))
|
||||
|
||||
;; Component (~name)
|
||||
(starts-with? name "~")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -589,13 +589,19 @@
|
||||
(define sx-render
|
||||
(fn (text)
|
||||
;; Parse SX text and render to a DOM fragment.
|
||||
;; Islands are rendered as empty markers (span with data-sx-island)
|
||||
;; — post-swap will hydrate them. This matches the server's aser mode
|
||||
;; where island calls are serialized without expansion.
|
||||
(let ((doc (host-global "document"))
|
||||
(frag (host-call doc "createDocumentFragment"))
|
||||
(exprs (sx-parse text)))
|
||||
;; Push marker mode: render-dom-island creates markers, not full renders
|
||||
(scope-push! "sx-render-markers" true)
|
||||
(for-each (fn (expr)
|
||||
(let ((result (render-to-dom expr (get-render-env nil) nil)))
|
||||
(when result (dom-append frag result))))
|
||||
exprs)
|
||||
(scope-pop! "sx-render-markers")
|
||||
frag)))
|
||||
|
||||
(define sx-hydrate
|
||||
|
||||
@@ -296,11 +296,11 @@
|
||||
;; Hydrated islands are preserved — the morph algorithm
|
||||
;; keeps their live signals and only morphs their lakes.
|
||||
(dispose-islands-in target)
|
||||
;; Swap
|
||||
;; Swap — swap-dom-nodes returns the new element for outerHTML
|
||||
(with-transition use-transition
|
||||
(fn ()
|
||||
(swap-dom-nodes target content swap-style)
|
||||
(post-swap target)))))))))))
|
||||
(let ((swap-result (swap-dom-nodes target content swap-style)))
|
||||
(post-swap (or swap-result target))))))))))))
|
||||
|
||||
|
||||
(define handle-html-response :effects [mutation io]
|
||||
|
||||
Reference in New Issue
Block a user