Add reactive islands spec: signals.sx + defisland across all adapters
New spec file signals.sx defines the signal runtime: signal, computed, effect, deref, reset!, swap!, batch, dispose, and island scope tracking. eval.sx: defisland special form + island? type predicate in eval-call. boundary.sx: signal primitive declarations (Tier 3). render.sx: defisland in definition-form?. adapter-dom.sx: render-dom-island with reactive context, reactive-text, reactive-attr, reactive-fragment, reactive-list helpers. adapter-html.sx: render-html-island for SSR with data-sx-island/state. adapter-sx.sx: island? handling in wire format serialization. special-forms.sx: defisland declaration with docs and example. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,12 @@
|
||||
(contains? HTML_TAGS name)
|
||||
(render-dom-element name args env ns)
|
||||
|
||||
;; Island (~name) — reactive component
|
||||
(and (starts-with? name "~")
|
||||
(env-has? env name)
|
||||
(island? (env-get env name)))
|
||||
(render-dom-island (env-get env name) args env ns)
|
||||
|
||||
;; Component (~name)
|
||||
(starts-with? name "~")
|
||||
(let ((comp (env-get env name)))
|
||||
@@ -284,7 +290,7 @@
|
||||
|
||||
(define RENDER_DOM_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defhandler"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-dom-form?
|
||||
@@ -414,6 +420,153 @@
|
||||
(render-to-dom (lambda-body f) local ns))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-island — render a reactive island
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Islands render like components but wrapped in a reactive context.
|
||||
;; The island container element gets data-sx-island and data-sx-state
|
||||
;; attributes for identification and hydration.
|
||||
;;
|
||||
;; Inside the island body, deref calls create reactive DOM subscriptions:
|
||||
;; - Text bindings: (deref sig) in text position → reactive text node
|
||||
;; - Attribute bindings: (deref sig) in attr → reactive attribute
|
||||
;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide
|
||||
|
||||
(define render-dom-island
|
||||
(fn (island args env ns)
|
||||
;; Parse kwargs and children (same as component)
|
||||
(let ((kwargs (dict))
|
||||
(children (list)))
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (trampoline
|
||||
(eval-expr (nth args (inc (get state "i"))) env))))
|
||||
(dict-set! kwargs (keyword-name arg) val)
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(do
|
||||
(append! children arg)
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
|
||||
;; Build island env: closure + caller env + params
|
||||
(let ((local (env-merge (component-closure island) env))
|
||||
(island-name (component-name island)))
|
||||
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params island))
|
||||
|
||||
;; If island accepts children, pre-render them to a fragment
|
||||
(when (component-has-children? island)
|
||||
(let ((child-frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (c) (dom-append child-frag (render-to-dom c env ns)))
|
||||
children)
|
||||
(env-set! local "children" child-frag)))
|
||||
|
||||
;; Create the island container element
|
||||
(let ((container (dom-create-element "div" nil))
|
||||
(disposers (list)))
|
||||
|
||||
;; Mark as island
|
||||
(dom-set-attr container "data-sx-island" island-name)
|
||||
|
||||
;; Render island body inside a scope that tracks disposers
|
||||
(let ((body-dom
|
||||
(with-island-scope
|
||||
(fn (disposable) (append! disposers disposable))
|
||||
(fn () (render-to-dom (component-body island) local ns)))))
|
||||
(dom-append container body-dom)
|
||||
|
||||
;; Store disposers on the container for cleanup
|
||||
(dom-set-data container "sx-disposers" disposers)
|
||||
|
||||
container))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Reactive DOM rendering helpers
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; These functions create reactive bindings between signals and DOM nodes.
|
||||
;; They are called by the platform's renderDOM when it detects deref
|
||||
;; calls inside an island context.
|
||||
|
||||
;; reactive-text — create a text node bound to a signal
|
||||
;; Used when (deref sig) appears in a text position inside an island.
|
||||
(define reactive-text
|
||||
(fn (sig)
|
||||
(let ((node (create-text-node (str (deref sig)))))
|
||||
(effect (fn ()
|
||||
(dom-set-text-content node (str (deref sig)))))
|
||||
node)))
|
||||
|
||||
;; reactive-attr — bind an element attribute to a signal expression
|
||||
;; Used when an attribute value contains (deref sig) inside an island.
|
||||
(define reactive-attr
|
||||
(fn (el attr-name compute-fn)
|
||||
(effect (fn ()
|
||||
(let ((val (compute-fn)))
|
||||
(cond
|
||||
(or (nil? val) (= val false))
|
||||
(dom-remove-attr el attr-name)
|
||||
(= val true)
|
||||
(dom-set-attr el attr-name "")
|
||||
:else
|
||||
(dom-set-attr el attr-name (str val))))))))
|
||||
|
||||
;; reactive-fragment — conditionally render a fragment based on a signal
|
||||
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
|
||||
(define reactive-fragment
|
||||
(fn (test-fn render-fn env ns)
|
||||
(let ((marker (create-comment "island-fragment"))
|
||||
(current-nodes (list)))
|
||||
(effect (fn ()
|
||||
;; Remove previous nodes
|
||||
(for-each (fn (n) (dom-remove n)) current-nodes)
|
||||
(set! current-nodes (list))
|
||||
;; If test passes, render and insert after marker
|
||||
(when (test-fn)
|
||||
(let ((frag (render-fn)))
|
||||
(set! current-nodes (dom-child-nodes frag))
|
||||
(dom-insert-after marker frag)))))
|
||||
marker)))
|
||||
|
||||
;; reactive-list — render a keyed list bound to a signal
|
||||
;; Used for (map fn (deref items)) inside an island.
|
||||
(define reactive-list
|
||||
(fn (map-fn items-sig env ns)
|
||||
(let ((container (create-fragment))
|
||||
(marker (create-comment "island-list")))
|
||||
(dom-append container marker)
|
||||
(effect (fn ()
|
||||
;; Simple strategy: clear and re-render
|
||||
;; Future: keyed reconciliation
|
||||
(let ((parent (dom-parent marker)))
|
||||
(when parent
|
||||
;; Remove all nodes after marker until next sibling marker
|
||||
(dom-remove-children-after marker)
|
||||
;; Render new items
|
||||
(let ((items (deref items-sig)))
|
||||
(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)))))))
|
||||
container)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — DOM adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -422,11 +575,20 @@
|
||||
;; (dom-create-element tag ns) → Element (ns=nil for HTML, string for SVG/MathML)
|
||||
;; (create-text-node s) → Text node
|
||||
;; (create-fragment) → DocumentFragment
|
||||
;; (create-comment s) → Comment node
|
||||
;;
|
||||
;; Tree mutation:
|
||||
;; (dom-append parent child) → void (appendChild)
|
||||
;; (dom-set-attr el name val) → void (setAttribute)
|
||||
;; (dom-remove-attr el name) → void (removeAttribute)
|
||||
;; (dom-get-attr el name) → string or nil (getAttribute)
|
||||
;; (dom-set-text-content n s) → void (set textContent)
|
||||
;; (dom-remove node) → void (remove from parent)
|
||||
;; (dom-insert-after ref node) → void (insert node after ref)
|
||||
;; (dom-parent node) → parent Element or nil
|
||||
;; (dom-child-nodes frag) → list of child nodes
|
||||
;; (dom-remove-children-after m)→ void (remove all siblings after marker)
|
||||
;; (dom-set-data el key val) → void (store arbitrary data on element)
|
||||
;;
|
||||
;; Content parsing:
|
||||
;; (dom-parse-html s) → DocumentFragment from HTML string
|
||||
@@ -441,11 +603,15 @@
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
;; env-has?, env-get, env-set!, env-merge
|
||||
;; lambda?, component?, macro?
|
||||
;; lambda?, component?, island?, macro?
|
||||
;; lambda-closure, lambda-params, lambda-body
|
||||
;; component-params, component-body, component-closure,
|
||||
;; component-has-children?, component-name
|
||||
;;
|
||||
;; From signals.sx:
|
||||
;; signal, deref, reset!, swap!, computed, effect, batch
|
||||
;; signal?, with-island-scope
|
||||
;;
|
||||
;; Iteration:
|
||||
;; (for-each-indexed fn coll) → call fn(index, item) for each element
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user