From 846719908f368d5a46ce7233c24455cee9fc6950 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 13 Mar 2026 04:51:05 +0000 Subject: [PATCH] Reactive forms pass spreads through instead of wrapping in fragments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adapter-dom.sx: if/when/cond reactive paths now check whether initial-result is a spread. If so, return it directly — spreads aren't DOM nodes and can't be appended to fragments. This lets any spread-returning component (like ~cssx/tw) work inside islands without the spread being silently dropped. cssx.sx: revert make-spread workaround — the root cause is now fixed in the adapter. ~cssx/tw can use a natural top-level if. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 14 ++++----- shared/sx/ref/adapter-dom.sx | 45 +++++++++++++++++------------ shared/sx/templates/cssx.sx | 10 +++---- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 808840e..8a3e988 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-13T04:16:14Z"; + var SX_VERSION = "2026-03-13T04:50:39Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -2019,7 +2019,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme })(); return (isSxTruthy(domParent(marker)) ? (forEach(function(n) { return domRemove(n); }, currentNodes), (currentNodes = (isSxTruthy(domIsFragment(result)) ? domChildNodes(result) : [result])), domInsertAfter(marker, result)) : (initialResult = result)); })(); }); - return (function() { + return (isSxTruthy(isSpread(initialResult)) ? initialResult : (function() { var frag = createFragment(); domAppend(frag, marker); if (isSxTruthy(initialResult)) { @@ -2027,7 +2027,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme domAppend(frag, initialResult); } return frag; -})(); +})()); })() : (function() { var condVal = trampoline(evalExpr(nth(expr, 1), env)); return (isSxTruthy(condVal) ? renderToDom(nth(expr, 2), env, ns) : (isSxTruthy((len(expr) > 3)) ? renderToDom(nth(expr, 3), env, ns) : createFragment())); @@ -2046,14 +2046,14 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme currentNodes = domChildNodes(frag); return (initialResult = frag); })() : NIL)); }); - return (function() { + return (isSxTruthy(isSpread(initialResult)) ? initialResult : (function() { var frag = createFragment(); domAppend(frag, marker); if (isSxTruthy(initialResult)) { domAppend(frag, initialResult); } return frag; -})(); +})()); })() : (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? createFragment() : (function() { var frag = createFragment(); { var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } } @@ -2074,14 +2074,14 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme return (initialResult = result); })() : NIL)); })(); }); - return (function() { + return (isSxTruthy(isSpread(initialResult)) ? initialResult : (function() { var frag = createFragment(); domAppend(frag, marker); if (isSxTruthy(initialResult)) { domAppend(frag, initialResult); } return frag; -})(); +})()); })() : (function() { var branch = evalCond(rest(expr), env); return (isSxTruthy(branch) ? renderToDom(branch, env, ns) : createFragment()); diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index 7c17907..2b3c315 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -406,16 +406,19 @@ (dom-insert-after marker result)) ;; Marker not yet in DOM (first run) — just save result (set! initial-result result))))) - ;; Return fragment: marker + initial render result - (let ((frag (create-fragment))) - (dom-append frag marker) - (when initial-result - (set! current-nodes - (if (dom-is-fragment? initial-result) - (dom-child-nodes initial-result) - (list initial-result))) - (dom-append frag initial-result)) - frag)) + ;; Spread pass-through: spreads aren't DOM nodes, can't live + ;; in fragments. Return directly so parent element merges attrs. + (if (spread? initial-result) + initial-result + (let ((frag (create-fragment))) + (dom-append frag marker) + (when initial-result + (set! current-nodes + (if (dom-is-fragment? initial-result) + (dom-child-nodes initial-result) + (list initial-result))) + (dom-append frag initial-result)) + frag))) ;; Static if (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val @@ -453,10 +456,13 @@ (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (set! initial-result frag)))))) - (let ((frag (create-fragment))) - (dom-append frag marker) - (when initial-result (dom-append frag initial-result)) - frag)) + ;; Spread pass-through + (if (spread? initial-result) + initial-result + (let ((frag (create-fragment))) + (dom-append frag marker) + (when initial-result (dom-append frag initial-result)) + frag))) ;; Static when (if (not (trampoline (eval-expr (nth expr 1) env))) (create-fragment) @@ -495,10 +501,13 @@ (dom-child-nodes result) (list result))) (set! initial-result result))))))) - (let ((frag (create-fragment))) - (dom-append frag marker) - (when initial-result (dom-append frag initial-result)) - frag)) + ;; Spread pass-through + (if (spread? initial-result) + initial-result + (let ((frag (create-fragment))) + (dom-append frag marker) + (when initial-result (dom-append frag initial-result)) + frag))) ;; Static cond (let ((branch (eval-cond (rest expr) env))) (if branch diff --git a/shared/sx/templates/cssx.sx b/shared/sx/templates/cssx.sx index f90264d..309d9e2 100644 --- a/shared/sx/templates/cssx.sx +++ b/shared/sx/templates/cssx.sx @@ -476,12 +476,10 @@ (classes (map (fn (r) (get r "cls")) valid)) (rules (map (fn (r) (get r "rule")) valid)) (_ (for-each (fn (rule) (collect! "cssx" rule)) rules))) - ;; Return spread: injects class + data-tw onto parent element. - ;; The if is inside make-spread's arg so it goes through eval-expr - ;; (not render-to-dom), avoiding reactive-if wrapping in islands. - (make-spread (if (empty? classes) - {} - {"class" (join " " classes) + ;; Return spread: injects class + data-tw onto parent element + (if (empty? classes) + nil + (make-spread {"class" (join " " classes) "data-tw" (or tokens "")}))))