SPA navigation, page component refactors, WASM rebuild
Refactor page components (docs, examples, specs, reference, layouts) and adapters (adapter-sx, boot-helpers, orchestration) across sx/ and web/ directories. Add Playwright SPA navigation tests. Rebuild WASM kernel with updated bytecode. Add OCaml primitives for request handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,58 +1,128 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Event Bridge — DOM events for lake→island communication
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~reactive-islands/event-bridge/reactive-islands-event-bridge-content ()
|
||||
(~docs/page :title "Event Bridge"
|
||||
|
||||
(~docs/section :title "The Problem" :id "problem"
|
||||
(p "A reactive island can contain server-rendered content — an htmx \"lake\" that swaps via " (code "sx-get") "/" (code "sx-post") ". The lake content is pure HTML from the server. It has no access to island signals.")
|
||||
(p "But sometimes the lake needs to " (em "tell") " the island something happened. A server-rendered \"Add to Cart\" button needs to update the island's cart signal. A server-rendered search form needs to feed results into the island's result signal.")
|
||||
(p "The event bridge solves this: DOM custom events bubble from the lake up to the island, where an effect listens and updates signals."))
|
||||
|
||||
(~docs/section :title "How it works" :id "how"
|
||||
(defcomp
|
||||
~reactive-islands/event-bridge/reactive-islands-event-bridge-content
|
||||
()
|
||||
(~docs/page
|
||||
:title "Event Bridge"
|
||||
(~docs/section
|
||||
:title "The Problem"
|
||||
:id "problem"
|
||||
(p
|
||||
"A reactive island can contain server-rendered content — an htmx \"lake\" that swaps via "
|
||||
(code "sx-get")
|
||||
"/"
|
||||
(code "sx-post")
|
||||
". The lake content is pure HTML from the server. It has no access to island signals.")
|
||||
(p
|
||||
"But sometimes the lake needs to "
|
||||
(em "tell")
|
||||
" the island something happened. A server-rendered \"Add to Cart\" button needs to update the island's cart signal. A server-rendered search form needs to feed results into the island's result signal.")
|
||||
(p
|
||||
"The event bridge solves this: DOM custom events bubble from the lake up to the island, where an effect listens and updates signals."))
|
||||
(~docs/section
|
||||
:title "How it works"
|
||||
:id "how"
|
||||
(p "Three components:")
|
||||
(ol :class "space-y-2 text-stone-600 list-decimal list-inside"
|
||||
(li (strong "Server emits: ") "Server-rendered elements carry " (code "data-sx-emit") " attributes. When the user interacts, the client dispatches a CustomEvent.")
|
||||
(li (strong "Event bubbles: ") "The event bubbles up through the DOM tree until it reaches the island container.")
|
||||
(li (strong "Effect catches: ") "An effect inside the island listens for the event name and updates a signal."))
|
||||
|
||||
(~docs/code :src (highlight ";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))" "lisp"))
|
||||
|
||||
(p "The server handler for " (code "/products/:id/details") " returns HTML with emit attributes:")
|
||||
(~docs/code :src (highlight ";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))" "lisp"))
|
||||
(p "The button is plain server HTML. When clicked, the client's event bridge dispatches " (code "cart:add") " with the JSON detail. The island effect catches it and appends to " (code "cart-items") ". The badge updates reactively."))
|
||||
|
||||
(~docs/section :title "Why signals survive swaps" :id "survival"
|
||||
(p "Signals live in JavaScript memory (closures), not in the DOM. When htmx swaps content inside an island:")
|
||||
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li (strong "Swap inside island: ") "Signals survive. The lake content is replaced but the island's signal closures are untouched. Effects re-bind to new DOM nodes if needed.")
|
||||
(li (strong "Swap outside island: ") "Signals survive. The island is not affected by swaps to other parts of the page.")
|
||||
(li (strong "Swap replaces island: ") "Signals are " (em "lost") ". The island is disposed. This is where " (a :href "/sx/(geography.(reactive.(named-stores)))" :sx-get "/sx/(geography.(reactive.(named-stores)))" :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-700 underline" "named stores") " come in — they persist at page level, surviving island destruction.")))
|
||||
|
||||
(~docs/section :title "Spec" :id "spec"
|
||||
(p "The event bridge is spec'd in " (code "signals.sx") " (sections 12-13). Three functions:")
|
||||
|
||||
(~docs/code :src (highlight ";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))" "lisp"))
|
||||
|
||||
(ol
|
||||
:class "space-y-2 text-stone-600 list-decimal list-inside"
|
||||
(li
|
||||
(strong "Server emits: ")
|
||||
"Server-rendered elements carry "
|
||||
(code "data-sx-emit")
|
||||
" attributes. When the user interacts, the client dispatches a CustomEvent.")
|
||||
(li
|
||||
(strong "Event bubbles: ")
|
||||
"The event bubbles up through the DOM tree until it reaches the island container.")
|
||||
(li
|
||||
(strong "Effect catches: ")
|
||||
"An effect inside the island listens for the event name and updates a signal."))
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; Island with an event bridge\n(defisland ~reactive-islands/event-bridge/product-page (&key product)\n (let ((cart-items (signal (list))))\n\n ;; Bridge: listen for \"cart:add\" events from server content\n (bridge-event container \"cart:add\" cart-items\n (fn (detail)\n (append (deref cart-items)\n (dict :id (get detail \"id\")\n :name (get detail \"name\")\n :price (get detail \"price\")))))\n\n (div\n ;; Island header with reactive cart count\n (div :class \"flex justify-between\"\n (h1 (get product \"name\"))\n (span :class \"badge\" (length (deref cart-items)) \" items\"))\n\n ;; htmx lake — server-rendered product details\n ;; This content is swapped by sx-get, not rendered by the island\n (div :id \"product-details\"\n :sx-get (str \"/products/\" (get product \"id\") \"/details\")\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\"))))"
|
||||
"lisp"))
|
||||
(p
|
||||
"The server handler for "
|
||||
(code "/products/:id/details")
|
||||
" returns HTML with emit attributes:")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; Server-rendered response (pure HTML, no signals)\n(div\n (p (get product \"description\"))\n (div :class \"flex gap-2 mt-4\"\n (button\n :data-sx-emit \"cart:add\"\n :data-sx-emit-detail (json-serialize\n (dict :id (get product \"id\")\n :name (get product \"name\")\n :price (get product \"price\")))\n :class \"bg-violet-600 text-white px-4 py-2 rounded\"\n \"Add to Cart\")))"
|
||||
"lisp"))
|
||||
(p
|
||||
"The button is plain server HTML. When clicked, the client's event bridge dispatches "
|
||||
(code "cart:add")
|
||||
" with the JSON detail. The island effect catches it and appends to "
|
||||
(code "cart-items")
|
||||
". The badge updates reactively."))
|
||||
(~docs/section
|
||||
:title "Why signals survive swaps"
|
||||
:id "survival"
|
||||
(p
|
||||
"Signals live in JavaScript memory (closures), not in the DOM. When htmx swaps content inside an island:")
|
||||
(ul
|
||||
:class "space-y-2 text-stone-600 list-disc pl-5"
|
||||
(li
|
||||
(strong "Swap inside island: ")
|
||||
"Signals survive. The lake content is replaced but the island's signal closures are untouched. Effects re-bind to new DOM nodes if needed.")
|
||||
(li
|
||||
(strong "Swap outside island: ")
|
||||
"Signals survive. The island is not affected by swaps to other parts of the page.")
|
||||
(li
|
||||
(strong "Swap replaces island: ")
|
||||
"Signals are "
|
||||
(em "lost")
|
||||
". The island is disposed. This is where "
|
||||
(a
|
||||
:href "/sx/(geography.(reactive.(named-stores)))"
|
||||
:sx-get "/sx/(geography.(reactive.(named-stores)))"
|
||||
:sx-target "#sx-content"
|
||||
:sx-select "#sx-content"
|
||||
:sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
:class "text-violet-700 underline"
|
||||
"named stores")
|
||||
" come in — they persist at page level, surviving island destruction.")))
|
||||
(~docs/section
|
||||
:title "Spec"
|
||||
:id "spec"
|
||||
(p
|
||||
"The event bridge is spec'd in "
|
||||
(code "signals.sx")
|
||||
" (sections 12-13). Three functions:")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; Low-level: dispatch a custom event\n(emit-event el \"cart:add\" {:id 42 :name \"Widget\"})\n\n;; Low-level: listen for a custom event\n(on-event container \"cart:add\" (fn (e)\n (swap! items (fn (old) (append old (event-detail e))))))\n\n;; High-level: bridge an event directly to a signal\n;; Creates an effect with automatic cleanup on dispose\n(bridge-event container \"cart:add\" items\n (fn (detail) (append (deref items) detail)))"
|
||||
"lisp"))
|
||||
(p "Platform interface required:")
|
||||
(div :class "overflow-x-auto rounded border border-stone-200 mt-2"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Function")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(dom-listen el name handler)")
|
||||
(td :class "px-3 py-2 text-stone-700" "Attach event listener, return remove function"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(dom-dispatch el name detail)")
|
||||
(td :class "px-3 py-2 text-stone-700" "Dispatch CustomEvent with detail, bubbles: true"))
|
||||
(div
|
||||
:class "overflow-x-auto rounded border border-stone-200 mt-2"
|
||||
(table
|
||||
:class "w-full text-left text-sm"
|
||||
(thead
|
||||
(tr
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "(event-detail e)")
|
||||
(td :class "px-3 py-2 text-stone-700" "Extract .detail from CustomEvent"))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Named Stores — page-level signal containers
|
||||
;; ---------------------------------------------------------------------------
|
||||
:class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Function")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
||||
(tbody
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-sm text-violet-700"
|
||||
"(dom-listen el name handler)")
|
||||
(td
|
||||
:class "px-3 py-2 text-stone-700"
|
||||
"Attach event listener, return remove function"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-sm text-violet-700"
|
||||
"(dom-dispatch el name detail)")
|
||||
(td
|
||||
:class "px-3 py-2 text-stone-700"
|
||||
"Dispatch CustomEvent with detail, bubbles: true"))
|
||||
(tr
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-sm text-violet-700"
|
||||
"(event-detail e)")
|
||||
(td
|
||||
:class "px-3 py-2 text-stone-700"
|
||||
"Extract .detail from CustomEvent"))))))))
|
||||
|
||||
Reference in New Issue
Block a user