Add Phase 2 P1 features: reactive class/style, refs, portals
- :class-map dict toggles classes reactively via classList.add/remove - :style-map dict sets inline styles reactively via el.style[prop] - ref/ref-get/ref-set! mutable boxes (non-reactive, like useRef) - :ref attribute sets ref.current to DOM element after rendering - portal render-dom form renders children into remote target element - Portal content auto-removed on island disposal via register-in-scope - Added #portal-root div to page shell template - Added stop-propagation and dom-focus platform functions - Demo islands for all three features on the demo page - Updated status tables: all P0/P1 features marked Done Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,15 @@
|
||||
;; Two-way input binding: :bind signal
|
||||
(and (= attr-name "bind") (signal? attr-val))
|
||||
(bind-input el attr-val)
|
||||
;; class-map: reactively toggle classes
|
||||
(= attr-name "class-map")
|
||||
(reactive-class-map el attr-val)
|
||||
;; style-map: reactively set inline styles
|
||||
(= attr-name "style-map")
|
||||
(reactive-style-map el attr-val)
|
||||
;; ref: set ref.current to this element
|
||||
(= attr-name "ref")
|
||||
(ref-set! attr-val el)
|
||||
;; Boolean attr
|
||||
(contains? BOOLEAN_ATTRS attr-name)
|
||||
(when attr-val (dom-set-attr el attr-name ""))
|
||||
@@ -306,7 +315,7 @@
|
||||
(define RENDER_DOM_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
"map" "map-indexed" "filter" "for-each" "portal"))
|
||||
|
||||
(define render-dom-form?
|
||||
(fn (name)
|
||||
@@ -423,6 +432,10 @@
|
||||
(= name "filter")
|
||||
(render-to-dom (trampoline (eval-expr expr env)) env ns)
|
||||
|
||||
;; portal — render children into a remote target element
|
||||
(= name "portal")
|
||||
(render-dom-portal args env ns)
|
||||
|
||||
;; for-each (render variant)
|
||||
(= name "for-each")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
@@ -700,6 +713,86 @@
|
||||
(reset! sig (dom-get-prop el "value"))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; reactive-class-map — toggle classes based on signals
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Dict values should be signals or booleans. Signals are deref'd reactively.
|
||||
;;
|
||||
;; (div :class-map (dict "active" selected? "hidden" hide-computed))
|
||||
;;
|
||||
;; Creates a single effect that deref's each value (tracking signal deps)
|
||||
;; and calls classList.add/remove for each class.
|
||||
|
||||
(define reactive-class-map
|
||||
(fn (el class-dict)
|
||||
(effect (fn ()
|
||||
(for-each
|
||||
(fn (cls)
|
||||
(let ((val (deref (get class-dict cls))))
|
||||
(if val
|
||||
(dom-add-class el cls)
|
||||
(dom-remove-class el cls))))
|
||||
(keys class-dict))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; reactive-style-map — reactively set inline styles via signals
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Dict values should be signals or strings. Signals are deref'd reactively.
|
||||
;;
|
||||
;; (div :style-map (dict "width" width-sig "opacity" opacity-computed))
|
||||
;;
|
||||
;; Creates a single effect that deref's each value and sets the style property.
|
||||
|
||||
(define reactive-style-map
|
||||
(fn (el style-dict)
|
||||
(effect (fn ()
|
||||
(for-each
|
||||
(fn (prop)
|
||||
(dom-set-style el prop (str (deref (get style-dict prop)))))
|
||||
(keys style-dict))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-portal — render children into a remote target element
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; (portal "#modal-root" (div "content"))
|
||||
;;
|
||||
;; Renders children into the DOM node matched by the selector, rather than
|
||||
;; into the current position. Returns a comment marker at the original
|
||||
;; position. Registers a disposer to clean up portal content on island
|
||||
;; teardown.
|
||||
|
||||
(define render-dom-portal
|
||||
(fn (args env ns)
|
||||
(let ((selector (trampoline (eval-expr (first args) env)))
|
||||
(target (dom-query selector)))
|
||||
(if (not target)
|
||||
;; Target not found — render nothing, log warning
|
||||
(do
|
||||
(log-warn (str "Portal target not found: " selector))
|
||||
(create-comment (str "portal: " selector " (not found)")))
|
||||
(let ((marker (create-comment (str "portal: " selector)))
|
||||
(frag (create-fragment)))
|
||||
;; Render children into the fragment
|
||||
(for-each
|
||||
(fn (child) (dom-append frag (render-to-dom child env ns)))
|
||||
(rest args))
|
||||
;; Track portal nodes for disposal
|
||||
(let ((portal-nodes (dom-child-nodes frag)))
|
||||
;; Append into remote target
|
||||
(dom-append target frag)
|
||||
;; Register disposer: remove portal content on island teardown
|
||||
(register-in-scope
|
||||
(fn ()
|
||||
(for-each (fn (n) (dom-remove n)) portal-nodes))))
|
||||
;; Return marker at original position
|
||||
marker)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — DOM adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -724,6 +817,20 @@
|
||||
;; (dom-set-data el key val) → void (store arbitrary data on element)
|
||||
;; (dom-get-data el key) → any (retrieve data stored on element)
|
||||
;;
|
||||
;; Property access (for input binding):
|
||||
;; (dom-set-prop el name val) → void (set JS property: el[name] = val)
|
||||
;; (dom-get-prop el name) → any (read JS property: el[name])
|
||||
;;
|
||||
;; Class manipulation (for reactive class-map):
|
||||
;; (dom-add-class el cls) → void (classList.add)
|
||||
;; (dom-remove-class el cls) → void (classList.remove)
|
||||
;;
|
||||
;; Style manipulation (for reactive style-map):
|
||||
;; (dom-set-style el prop val) → void (el.style[prop] = val)
|
||||
;;
|
||||
;; Query (for portals):
|
||||
;; (dom-query selector) → Element or nil (document.querySelector)
|
||||
;;
|
||||
;; Event handling:
|
||||
;; (dom-listen el name handler) → remove-fn (addEventListener, returns remover)
|
||||
;; (dom-dispatch el name detail)→ boolean (dispatch CustomEvent, bubbles: true)
|
||||
@@ -748,7 +855,11 @@
|
||||
;;
|
||||
;; From signals.sx:
|
||||
;; signal, deref, reset!, swap!, computed, effect, batch
|
||||
;; signal?, with-island-scope
|
||||
;; signal?, with-island-scope, register-in-scope
|
||||
;; ref, ref-get, ref-set!
|
||||
;;
|
||||
;; Pure primitives used:
|
||||
;; keys, get, str
|
||||
;;
|
||||
;; Iteration:
|
||||
;; (for-each-indexed fn coll) → call fn(index, item) for each element
|
||||
|
||||
@@ -444,6 +444,8 @@ class JSEmitter:
|
||||
"dom-outer-html": "domOuterHtml",
|
||||
"dom-body-inner-html": "domBodyInnerHtml",
|
||||
"prevent-default": "preventDefault_",
|
||||
"stop-propagation": "stopPropagation_",
|
||||
"dom-focus": "domFocus",
|
||||
"element-value": "elementValue",
|
||||
"validate-for-request": "validateForRequest",
|
||||
"with-transition": "withTransition",
|
||||
@@ -3461,6 +3463,8 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
// --- Events ---
|
||||
|
||||
function preventDefault_(e) { if (e && e.preventDefault) e.preventDefault(); }
|
||||
function stopPropagation_(e) { if (e && e.stopPropagation) e.stopPropagation(); }
|
||||
function domFocus(el) { if (el && el.focus) el.focus(); }
|
||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||
|
||||
function domAddListener(el, event, fn, opts) {
|
||||
|
||||
@@ -306,7 +306,28 @@
|
||||
|
||||
|
||||
;; ==========================================================================
|
||||
;; 12. Named stores — page-level signal containers (L3)
|
||||
;; 12. Refs — mutable boxes, no reactivity
|
||||
;; ==========================================================================
|
||||
;;
|
||||
;; A ref is a mutable container that does NOT trigger subscriptions when
|
||||
;; written. Like React's useRef: holds mutable values between renders, and
|
||||
;; provides imperative DOM element access via :ref attribute.
|
||||
|
||||
(define ref
|
||||
(fn (initial)
|
||||
(dict "current" initial)))
|
||||
|
||||
(define ref-get
|
||||
(fn (r)
|
||||
(get r "current")))
|
||||
|
||||
(define ref-set!
|
||||
(fn (r v)
|
||||
(dict-set! r "current" v)))
|
||||
|
||||
|
||||
;; ==========================================================================
|
||||
;; 13. Named stores — page-level signal containers (L3)
|
||||
;; ==========================================================================
|
||||
;;
|
||||
;; Stores persist across island creation/destruction. They live at page
|
||||
|
||||
Reference in New Issue
Block a user