Island disposal, reactive lists, input binding, and Phase 2 plan
- Effect and computed auto-register disposers with island scope via register-in-scope; dispose-islands-in called before every swap point (orchestration.sx) to clean up intervals/subscriptions on navigation. - Map + deref inside islands auto-upgrades to reactive-list for signal- bound list rendering. Demo island with add/remove items. - New :bind attribute for two-way signal-input binding (text, checkbox, radio, textarea, select). bind-input in adapter-dom.sx handles both signal→element (effect) and element→signal (event listener). - Phase 2 plan page at /reactive-islands/phase2 covering input binding, keyed reconciliation, reactive class/style, refs, portals, error boundaries, suspense, and transitions. - Updated status tables in overview and plan pages. - Fixed stopwatch reset (fn body needs do wrapper for multiple exprs). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,9 @@
|
||||
(and (starts-with? attr-name "on-")
|
||||
(callable? attr-val))
|
||||
(dom-listen el (slice attr-name 3) attr-val)
|
||||
;; Two-way input binding: :bind signal
|
||||
(and (= attr-name "bind") (signal? attr-val))
|
||||
(bind-input el attr-val)
|
||||
;; Boolean attr
|
||||
(contains? BOOLEAN_ATTRS attr-name)
|
||||
(when attr-val (dom-set-attr el attr-name ""))
|
||||
@@ -366,19 +369,41 @@
|
||||
(definition-form? name)
|
||||
(do (trampoline (eval-expr expr env)) (create-fragment))
|
||||
|
||||
;; map
|
||||
;; map — reactive-list when mapping over a signal inside an island
|
||||
(= name "map")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env)))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((val (if (lambda? f)
|
||||
(render-lambda-dom f (list item) env ns)
|
||||
(render-to-dom (apply f (list item)) env ns))))
|
||||
(dom-append frag val)))
|
||||
coll)
|
||||
frag)
|
||||
(let ((coll-expr (nth expr 2)))
|
||||
(if (and *island-scope*
|
||||
(= (type-of coll-expr) "list")
|
||||
(> (len coll-expr) 1)
|
||||
(= (first coll-expr) "deref"))
|
||||
;; Reactive path: pass signal to reactive-list
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(sig (trampoline (eval-expr (nth coll-expr 1) env))))
|
||||
(if (signal? sig)
|
||||
(reactive-list f sig env ns)
|
||||
;; deref on non-signal: fall through to static
|
||||
(let ((coll (deref sig))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((val (if (lambda? f)
|
||||
(render-lambda-dom f (list item) env ns)
|
||||
(render-to-dom (apply f (list item)) env ns))))
|
||||
(dom-append frag val)))
|
||||
coll)
|
||||
frag)))
|
||||
;; Static path: no island scope or no deref
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env)))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((val (if (lambda? f)
|
||||
(render-lambda-dom f (list item) env ns)
|
||||
(render-to-dom (apply f (list item)) env ns))))
|
||||
(dom-append frag val)))
|
||||
coll)
|
||||
frag)))
|
||||
|
||||
;; map-indexed
|
||||
(= name "map-indexed")
|
||||
@@ -567,18 +592,54 @@
|
||||
(when parent
|
||||
;; Remove all nodes after marker until next sibling marker
|
||||
(dom-remove-children-after marker)
|
||||
;; Render new items
|
||||
(let ((items (deref items-sig)))
|
||||
;; Render new items into a fragment, then insert after marker
|
||||
(let ((items (deref items-sig))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((rendered (if (lambda? map-fn)
|
||||
(render-lambda-dom map-fn (list item) env ns)
|
||||
(render-to-dom (apply map-fn (list item)) env ns))))
|
||||
(dom-insert-after marker rendered)))
|
||||
(reverse items)))))))
|
||||
(dom-append frag rendered)))
|
||||
items)
|
||||
(dom-insert-after marker frag))))))
|
||||
container)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; bind-input — two-way signal binding for form elements
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; (bind-input el sig) creates a bidirectional link:
|
||||
;; Signal → element: effect updates el.value (or el.checked) when sig changes
|
||||
;; Element → signal: input/change listener updates sig when user types
|
||||
;;
|
||||
;; Handles: input[text/number/email/...], textarea, select, checkbox, radio
|
||||
|
||||
(define bind-input
|
||||
(fn (el sig)
|
||||
(let ((input-type (lower (or (dom-get-attr el "type") "")))
|
||||
(is-checkbox (or (= input-type "checkbox")
|
||||
(= input-type "radio"))))
|
||||
;; Set initial value from signal
|
||||
(if is-checkbox
|
||||
(dom-set-prop el "checked" (deref sig))
|
||||
(dom-set-prop el "value" (str (deref sig))))
|
||||
;; Signal → element (reactive effect)
|
||||
(effect (fn ()
|
||||
(if is-checkbox
|
||||
(dom-set-prop el "checked" (deref sig))
|
||||
(let ((v (str (deref sig))))
|
||||
(when (!= (dom-get-prop el "value") v)
|
||||
(dom-set-prop el "value" v))))))
|
||||
;; Element → signal (event listener)
|
||||
(dom-listen el (if is-checkbox "change" "input")
|
||||
(fn (e)
|
||||
(if is-checkbox
|
||||
(reset! sig (dom-get-prop el "checked"))
|
||||
(reset! sig (dom-get-prop el "value"))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — DOM adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user