Remove reactive class/style (CSSX covers it), add error boundaries + resource
Reactive class/style (:class-map, :style-map) removed — CSSX components already handle dynamic class/style via defcomp with full SX logic. Added: - error-boundary render-dom form: try/catch around children, renders fallback fn with (err retry) on failure, disposes partial effects - resource async signal: wraps promise into signal with loading/data/error states, transitions automatically on resolve/reject - try-catch, error-message, promise-then platform functions - Updated Phase 2 status tables and demo page numbering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -185,12 +185,6 @@
|
||||
;; 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)
|
||||
@@ -315,7 +309,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" "portal"))
|
||||
"map" "map-indexed" "filter" "for-each" "portal" "error-boundary"))
|
||||
|
||||
(define render-dom-form?
|
||||
(fn (name)
|
||||
@@ -436,6 +430,10 @@
|
||||
(= name "portal")
|
||||
(render-dom-portal args env ns)
|
||||
|
||||
;; error-boundary — catch errors, render fallback
|
||||
(= name "error-boundary")
|
||||
(render-dom-error-boundary args env ns)
|
||||
|
||||
;; for-each (render variant)
|
||||
(= name "for-each")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
@@ -713,48 +711,6 @@
|
||||
(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
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -793,6 +749,80 @@
|
||||
marker)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-error-boundary — catch errors, render fallback UI
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; (error-boundary fallback-fn body...)
|
||||
;;
|
||||
;; Renders body children inside a try/catch. If any child throws during
|
||||
;; rendering, the fallback function is called with the error object, and
|
||||
;; its result is rendered instead. Effects within the boundary are disposed
|
||||
;; on error.
|
||||
;;
|
||||
;; The fallback function receives the error and a retry thunk:
|
||||
;; (fn (err retry) ...)
|
||||
;; Calling (retry) re-renders the body, replacing the fallback.
|
||||
|
||||
(define render-dom-error-boundary
|
||||
(fn (args env ns)
|
||||
(let ((fallback-expr (first args))
|
||||
(body-exprs (rest args))
|
||||
(container (dom-create-element "div" nil))
|
||||
(boundary-disposers (list)))
|
||||
(dom-set-attr container "data-sx-boundary" "true")
|
||||
|
||||
;; Render body with its own island scope for disposal
|
||||
(let ((render-body
|
||||
(fn ()
|
||||
;; Dispose old boundary content
|
||||
(for-each (fn (d) (d)) boundary-disposers)
|
||||
(set! boundary-disposers (list))
|
||||
|
||||
;; Clear container
|
||||
(dom-set-prop container "innerHTML" "")
|
||||
|
||||
;; Try to render body
|
||||
(try-catch
|
||||
(fn ()
|
||||
;; Render body children, tracking disposers
|
||||
(with-island-scope
|
||||
(fn (disposable)
|
||||
(append! boundary-disposers disposable)
|
||||
(register-in-scope disposable))
|
||||
(fn ()
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (child)
|
||||
(dom-append frag (render-to-dom child env ns)))
|
||||
body-exprs)
|
||||
(dom-append container frag)))))
|
||||
(fn (err)
|
||||
;; Dispose any partially-created effects
|
||||
(for-each (fn (d) (d)) boundary-disposers)
|
||||
(set! boundary-disposers (list))
|
||||
|
||||
;; Render fallback with error + retry
|
||||
(let ((fallback-fn (trampoline (eval-expr fallback-expr env)))
|
||||
(retry-fn (fn () (render-body))))
|
||||
(let ((fallback-dom
|
||||
(if (lambda? fallback-fn)
|
||||
(render-lambda-dom fallback-fn (list err retry-fn) env ns)
|
||||
(render-to-dom (apply fallback-fn (list err retry-fn)) env ns))))
|
||||
(dom-append container fallback-dom))))))))
|
||||
|
||||
;; Initial render
|
||||
(render-body)
|
||||
|
||||
;; Register boundary disposers with parent island scope
|
||||
(register-in-scope
|
||||
(fn ()
|
||||
(for-each (fn (d) (d)) boundary-disposers)
|
||||
(set! boundary-disposers (list))))
|
||||
|
||||
container))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — DOM adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -821,13 +851,6 @@
|
||||
;; (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)
|
||||
;;
|
||||
|
||||
@@ -411,6 +411,7 @@ class JSEmitter:
|
||||
"engine-init": "engineInit",
|
||||
# engine orchestration platform
|
||||
"promise-resolve": "promiseResolve",
|
||||
"promise-then": "promiseThen",
|
||||
"promise-catch": "promiseCatch",
|
||||
"abort-previous": "abortPrevious",
|
||||
"track-controller": "trackController",
|
||||
@@ -446,6 +447,8 @@ class JSEmitter:
|
||||
"prevent-default": "preventDefault_",
|
||||
"stop-propagation": "stopPropagation_",
|
||||
"dom-focus": "domFocus",
|
||||
"try-catch": "tryCatch",
|
||||
"error-message": "errorMessage",
|
||||
"element-value": "elementValue",
|
||||
"validate-for-request": "validateForRequest",
|
||||
"with-transition": "withTransition",
|
||||
@@ -3465,6 +3468,12 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
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 tryCatch(tryFn, catchFn) {
|
||||
try { return tryFn(); } catch (e) { return catchFn(e); }
|
||||
}
|
||||
function errorMessage(e) {
|
||||
return e && e.message ? e.message : String(e);
|
||||
}
|
||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||
|
||||
function domAddListener(el, event, fn, opts) {
|
||||
|
||||
@@ -408,3 +408,33 @@
|
||||
(reset! target-signal new-val))))))
|
||||
;; Return cleanup — removes listener on dispose/re-run
|
||||
remove)))))
|
||||
|
||||
|
||||
;; ==========================================================================
|
||||
;; 15. Resource — async signal with loading/resolved/error states
|
||||
;; ==========================================================================
|
||||
;;
|
||||
;; A resource wraps an async operation (fetch, computation) and exposes
|
||||
;; its state as a signal. The signal transitions through:
|
||||
;; {:loading true :data nil :error nil} — initial/loading
|
||||
;; {:loading false :data result :error nil} — success
|
||||
;; {:loading false :data nil :error err} — failure
|
||||
;;
|
||||
;; Usage:
|
||||
;; (let ((user (resource (fn () (fetch-json "/api/user")))))
|
||||
;; (cond
|
||||
;; (get (deref user) "loading") (div "Loading...")
|
||||
;; (get (deref user) "error") (div "Error: " (get (deref user) "error"))
|
||||
;; :else (div (get (deref user) "data"))))
|
||||
;;
|
||||
;; Platform interface required:
|
||||
;; (promise-then promise on-resolve on-reject) → void
|
||||
|
||||
(define resource
|
||||
(fn (fetch-fn)
|
||||
(let ((state (signal (dict "loading" true "data" nil "error" nil))))
|
||||
;; Kick off the async operation
|
||||
(promise-then (invoke fetch-fn)
|
||||
(fn (data) (reset! state (dict "loading" false "data" data "error" nil)))
|
||||
(fn (err) (reset! state (dict "loading" false "data" nil "error" err))))
|
||||
state)))
|
||||
|
||||
Reference in New Issue
Block a user