Wire reactive islands end-to-end: live interactive demos on the demo page
- Rebuild sx-browser.js with signals spec module (was missing entirely) - Register signal functions (signal, deref, effect, computed, etc.) as PRIMITIVES so runtime-evaluated SX code in island bodies can call them - Add reactive deref detection in adapter-dom.sx: (deref sig) in island scope creates reactive-text node instead of static text - Add Island SSR support in html.py (_render_island with data-sx-island) - Add Island bundling in jinja_bridge.py (defisland defs sent to client) - Update deps.py to track Island dependencies alongside Component - Add defisland to _ASER_FORMS in async_eval.py - Add clear-interval platform primitive (was missing) - Create four live demo islands: counter, temperature, imperative, stopwatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,100 @@
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "reactive-list morph"))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Live demo islands
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 1. Counter — basic signal + effect
|
||||
(defisland ~demo-counter (&key initial)
|
||||
(let ((count (signal (or initial 0)))
|
||||
(doubled (computed (fn () (* 2 (deref count))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(div :class "flex items-center gap-4"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! count dec))
|
||||
"−")
|
||||
(span :class "text-2xl font-bold text-violet-900 w-12 text-center"
|
||||
(deref count))
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! count inc))
|
||||
"+"))
|
||||
(p :class "text-sm text-stone-500 mt-2"
|
||||
"doubled: " (span :class "font-mono text-violet-700" (deref doubled))))))
|
||||
|
||||
;; 2. Temperature converter — computed derived signal
|
||||
(defisland ~demo-temperature ()
|
||||
(let ((celsius (signal 20))
|
||||
(fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(div :class "flex items-center gap-3"
|
||||
(div :class "flex items-center gap-2"
|
||||
(button :class "px-2 py-1 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300"
|
||||
:on-click (fn (e) (swap! celsius (fn (c) (- c 5))))
|
||||
"−5")
|
||||
(span :class "font-mono text-lg font-bold text-violet-900 w-16 text-center"
|
||||
(deref celsius))
|
||||
(button :class "px-2 py-1 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300"
|
||||
:on-click (fn (e) (swap! celsius (fn (c) (+ c 5))))
|
||||
"+5")
|
||||
(span :class "text-stone-500" "°C"))
|
||||
(span :class "text-stone-400" "=")
|
||||
(span :class "font-mono text-lg font-bold text-violet-900"
|
||||
(deref fahrenheit))
|
||||
(span :class "text-stone-500" "°F")))))
|
||||
|
||||
;; 3. Imperative counter — shows create-text-node + effect pattern
|
||||
(defisland ~demo-imperative ()
|
||||
(let ((count (signal 0))
|
||||
(text-node (create-text-node "0"))
|
||||
(_eff (effect (fn ()
|
||||
(dom-set-text-content text-node (str (deref count)))))))
|
||||
(div :class "rounded border border-stone-200 bg-stone-50 p-4 my-4"
|
||||
(p :class "text-sm text-stone-600 mb-2" "Imperative style — explicit " (code "effect") " + " (code "create-text-node") ":")
|
||||
(div :class "flex items-center gap-4"
|
||||
(button :class "px-3 py-1 rounded bg-stone-600 text-white text-sm font-medium hover:bg-stone-700"
|
||||
:on-click (fn (e) (swap! count dec))
|
||||
"−")
|
||||
(span :class "text-2xl font-bold text-stone-900 w-12 text-center"
|
||||
text-node)
|
||||
(button :class "px-3 py-1 rounded bg-stone-600 text-white text-sm font-medium hover:bg-stone-700"
|
||||
:on-click (fn (e) (swap! count inc))
|
||||
"+")))))
|
||||
|
||||
;; 4. Stopwatch — effect with cleanup (interval), fully imperative
|
||||
(defisland ~demo-stopwatch ()
|
||||
(let ((running (signal false))
|
||||
(elapsed (signal 0))
|
||||
(time-text (create-text-node "0.0s"))
|
||||
(btn-text (create-text-node "Start"))
|
||||
;; Timer effect — creates/clears interval based on running signal
|
||||
(_e1 (effect (fn ()
|
||||
(when (deref running)
|
||||
(let ((id (set-interval (fn () (swap! elapsed inc)) 100)))
|
||||
(fn () (clear-interval id)))))))
|
||||
;; Display effect
|
||||
(_e2 (effect (fn ()
|
||||
(let ((e (deref elapsed)))
|
||||
(dom-set-text-content time-text
|
||||
(str (floor (/ e 10)) "." (mod e 10) "s"))))))
|
||||
;; Button label effect
|
||||
(_e3 (effect (fn ()
|
||||
(dom-set-text-content btn-text
|
||||
(if (deref running) "Stop" "Start"))))))
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 my-4"
|
||||
(div :class "flex items-center gap-4"
|
||||
(span :class "font-mono text-2xl font-bold text-violet-900 w-24 text-center"
|
||||
time-text)
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm font-medium hover:bg-violet-700"
|
||||
:on-click (fn (e) (swap! running not))
|
||||
btn-text)
|
||||
(button :class "px-3 py-1 rounded bg-stone-300 text-stone-700 text-sm hover:bg-stone-400"
|
||||
:on-click (fn (e)
|
||||
(reset! running false)
|
||||
(reset! elapsed 0))
|
||||
"Reset")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Demo page — shows what's been implemented
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -135,33 +229,37 @@
|
||||
(~doc-page :title "Reactive Islands Demo"
|
||||
|
||||
(~doc-section :title "What this demonstrates" :id "what"
|
||||
(p "Everything below runs on signal primitives " (strong "transpiled from the SX spec") ". The signal runtime is defined in " (code "signals.sx") " (291 lines of s-expressions), then bootstrapped to JavaScript by " (code "bootstrap_js.py") ". No hand-written signal logic in JavaScript.")
|
||||
(p "The transpiled " (code "sx-ref.js") " exports " (code "Sx.signal") ", " (code "Sx.deref") ", " (code "Sx.reset") ", " (code "Sx.swap") ", " (code "Sx.computed") ", " (code "Sx.effect") ", and " (code "Sx.batch") " — all generated from the spec."))
|
||||
(p (strong "These are live interactive islands") " — not static code snippets. Click the buttons. The signal runtime is defined in " (code "signals.sx") " (374 lines of s-expressions), then bootstrapped to JavaScript by " (code "bootstrap_js.py") ". No hand-written signal logic in JavaScript.")
|
||||
(p "The transpiled " (code "sx-browser.js") " registers " (code "signal") ", " (code "deref") ", " (code "reset!") ", " (code "swap!") ", " (code "computed") ", " (code "effect") ", and " (code "batch") " as SX primitives — callable from " (code "defisland") " bodies defined in " (code ".sx") " files."))
|
||||
|
||||
(~doc-section :title "1. Signal + Computed + Effect" :id "demo-counter"
|
||||
(p "A signal holds a value. A computed derives from it. Effects subscribe to both and update the DOM when either changes.")
|
||||
(~doc-code :code (highlight "(define count (signal 0))\n(define doubled (computed (fn () (* 2 (deref count)))))\n\n;; Effect subscribes to count, updates DOM\n(effect (fn ()\n (dom-set-text-content display (deref count))))\n\n;; Effect subscribes to doubled, updates DOM\n(effect (fn ()\n (dom-set-text-content doubled-display (str \"doubled: \" (deref doubled)))))\n\n;; swap! updates count, both effects re-run\n(swap! count inc) ;; display shows 1, doubled-display shows \"doubled: 2\"" "lisp"))
|
||||
(p "The counter increments. The doubled value updates automatically. Each effect only re-runs when its specific dependencies change. No virtual DOM. No diffing."))
|
||||
(p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
|
||||
(~demo-counter :initial 0)
|
||||
(~doc-code :code (highlight "(defisland ~demo-counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"−\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
|
||||
(p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing."))
|
||||
|
||||
(~doc-section :title "2. Batch" :id "demo-batch"
|
||||
(p "Without batch, two signal writes trigger two effect runs. With batch, writes are deferred and subscribers notified once at the end.")
|
||||
(~doc-code :code (highlight ";; Without batch: 2 writes = 2 effect runs\n(reset! first 1) ;; effect runs\n(reset! second 2) ;; effect runs again\n\n;; With batch: 2 writes = 1 effect run\n(batch (fn ()\n (reset! first 1)\n (reset! second 2))) ;; effect runs once" "lisp"))
|
||||
(p "Batch deduplicates subscribers across all queued signals. If two signals notify the same effect, it runs once, not twice."))
|
||||
(~doc-section :title "2. Temperature Converter" :id "demo-temperature"
|
||||
(p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
|
||||
(~demo-temperature)
|
||||
(~doc-code :code (highlight "(defisland ~demo-temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"−5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
|
||||
(p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes."))
|
||||
|
||||
(~doc-section :title "3. Effect with cleanup" :id "demo-effect"
|
||||
(p "An effect can return a cleanup function. The cleanup runs before the effect re-runs (when dependencies change) and when the effect is disposed.")
|
||||
(~doc-code :code (highlight "(effect (fn ()\n (let ((active (deref polling)))\n (when active\n (let ((id (set-interval poll-fn 500)))\n ;; Return cleanup — runs before next re-run or on dispose\n (fn () (clear-interval id)))))))" "lisp"))
|
||||
(p "This mirrors React's " (code "useEffect") " cleanup pattern, but without the hook rules. The effect can be created anywhere — in a conditional, in a loop, in a closure."))
|
||||
(~doc-section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch"
|
||||
(p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.")
|
||||
(~demo-stopwatch)
|
||||
(~doc-code :code (highlight "(defisland ~demo-stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
|
||||
(p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order."))
|
||||
|
||||
(~doc-section :title "4. Computed chains" :id "demo-chain"
|
||||
(p "Computed signals can depend on other computed signals. The dependency graph builds itself via " (code "deref") " calls during evaluation.")
|
||||
(~doc-code :code (highlight "(define base (signal 1))\n(define doubled (computed (fn () (* 2 (deref base)))))\n(define quadrupled (computed (fn () (* 2 (deref doubled)))))\n\n;; Change base, both derived signals update\n(reset! base 3)\n(deref quadrupled) ;; => 12" "lisp"))
|
||||
(p "Three-level dependency chain. When " (code "base") " changes, " (code "doubled") " recomputes, which triggers " (code "quadrupled") " to recompute. Each computed only recomputes if its actual value changed — " (code "identical?") " check prevents unnecessary propagation."))
|
||||
(~doc-section :title "4. Imperative Pattern" :id "demo-imperative"
|
||||
(p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".")
|
||||
(~demo-imperative)
|
||||
(~doc-code :code (highlight "(defisland ~demo-imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
|
||||
(p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates."))
|
||||
|
||||
(~doc-section :title "5. defisland" :id "demo-island"
|
||||
(p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with an island flag that triggers reactive rendering.")
|
||||
(~doc-code :code (highlight "(defisland ~counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div :class \"counter\"\n (span :class \"text-2xl font-bold\" (deref count))\n (div :class \"flex gap-2 mt-2\"\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (button :on-click (fn (e) (swap! count dec)) \"-\")))))\n\n;; Server renders static HTML:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\": 0}'>\n;; <span class=\"text-2xl font-bold\">0</span>\n;; <div class=\"flex gap-2 mt-2\">\n;; <button>+</button> <button>-</button>\n;; </div>\n;; </div>" "lisp"))
|
||||
(p "The island is self-contained. " (code "count") " is local state. Buttons modify it. The span updates. Nothing outside the island is affected. No server round-trip."))
|
||||
(~doc-section :title "5. How defisland Works" :id "demo-island"
|
||||
(p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with a reactive boundary. Inside an island, " (code "deref") " subscribes DOM nodes to signals.")
|
||||
(~doc-code :code (highlight ";; Definition — same syntax as defcomp\n(defisland ~counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp"))
|
||||
(p "Each " (code "deref") " call registers the enclosing DOM node as a subscriber. Signal changes update " (em "only") " the subscribed nodes — no virtual DOM, no diffing, no component re-renders."))
|
||||
|
||||
(~doc-section :title "6. Test suite" :id "demo-tests"
|
||||
(p "17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).")
|
||||
|
||||
Reference in New Issue
Block a user