Add reactive spreads — signal-driven attribute injection in islands
When a spread value (e.g. from ~cssx/tw) appears inside an island with signal-dependent tokens, reactive-spread tracks deps and updates the element's class/attrs when signals change. Old classes are surgically removed, new ones appended, and freshly collected CSS rules are flushed to the live stylesheet. Multiple reactive spreads on one element are safe. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-13T04:50:39Z";
|
||||
var SX_VERSION = "2026-03-13T05:11:12Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -1934,7 +1934,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? (function() {
|
||||
var child = renderToDom(arg, env, newNs);
|
||||
return (isSxTruthy(isSpread(child)) ? forEach(function(key) { return (function() {
|
||||
return (isSxTruthy((isSxTruthy(isSpread(child)) && _islandScope)) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? forEach(function(key) { return (function() {
|
||||
var val = dictGet(spreadAttrs(child), key);
|
||||
return (isSxTruthy((key == "class")) ? (function() {
|
||||
var existing = domGetAttr(el, "class");
|
||||
@@ -1943,7 +1943,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
var existing = domGetAttr(el, "style");
|
||||
return domSetAttr(el, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val));
|
||||
})() : domSetAttr(el, key, (String(val)))));
|
||||
})(); }, keys(spreadAttrs(child))) : domAppend(el, child));
|
||||
})(); }, keys(spreadAttrs(child))) : domAppend(el, child)));
|
||||
})() : NIL), assoc(state, "i", (get(state, "i") + 1)))));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
return el;
|
||||
@@ -2269,6 +2269,44 @@ return effect(function() { return (function() {
|
||||
})();
|
||||
})(); }); };
|
||||
|
||||
// reactive-spread
|
||||
var reactiveSpread = function(el, renderFn) { return (function() {
|
||||
var prevClasses = [];
|
||||
var prevExtraKeys = [];
|
||||
(function() {
|
||||
var existing = sxOr(domGetAttr(el, "data-sx-reactive-attrs"), "");
|
||||
return domSetAttr(el, "data-sx-reactive-attrs", (isSxTruthy(isEmpty(existing)) ? "_spread" : (String(existing) + String(",_spread"))));
|
||||
})();
|
||||
return effect(function() { if (isSxTruthy(!isSxTruthy(isEmpty(prevClasses)))) {
|
||||
(function() {
|
||||
var current = sxOr(domGetAttr(el, "class"), "");
|
||||
var tokens = filter(function(c) { return !isSxTruthy((c == "")); }, split(current, " "));
|
||||
var kept = filter(function(c) { return !isSxTruthy(some(function(pc) { return (pc == c); }, prevClasses)); }, tokens);
|
||||
return (isSxTruthy(isEmpty(kept)) ? domRemoveAttr(el, "class") : domSetAttr(el, "class", join(" ", kept)));
|
||||
})();
|
||||
}
|
||||
{ var _c = prevExtraKeys; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; domRemoveAttr(el, k); } }
|
||||
return (function() {
|
||||
var result = renderFn();
|
||||
return (isSxTruthy(isSpread(result)) ? (function() {
|
||||
var attrs = spreadAttrs(result);
|
||||
var clsStr = sxOr(dictGet(attrs, "class"), "");
|
||||
var newClasses = filter(function(c) { return !isSxTruthy((c == "")); }, split(clsStr, " "));
|
||||
var extraKeys = filter(function(k) { return !isSxTruthy((k == "class")); }, keys(attrs));
|
||||
prevClasses = newClasses;
|
||||
prevExtraKeys = extraKeys;
|
||||
if (isSxTruthy(!isSxTruthy(isEmpty(newClasses)))) {
|
||||
(function() {
|
||||
var current = sxOr(domGetAttr(el, "class"), "");
|
||||
return domSetAttr(el, "class", (isSxTruthy((isSxTruthy(current) && !isSxTruthy((current == "")))) ? (String(current) + String(" ") + String(clsStr)) : clsStr));
|
||||
})();
|
||||
}
|
||||
{ var _c = extraKeys; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; domSetAttr(el, k, (String(dictGet(attrs, k)))); } }
|
||||
return flushCssxToDom();
|
||||
})() : ((prevClasses = []), (prevExtraKeys = [])));
|
||||
})(); });
|
||||
})(); };
|
||||
|
||||
// reactive-fragment
|
||||
var reactiveFragment = function(testFn, renderFn, env, ns) { return (function() {
|
||||
var marker = createComment("island-fragment");
|
||||
|
||||
@@ -232,30 +232,35 @@
|
||||
(do
|
||||
(when (not (contains? VOID_ELEMENTS tag))
|
||||
(let ((child (render-to-dom arg env new-ns)))
|
||||
(if (spread? child)
|
||||
;; Spread: merge attrs onto parent element
|
||||
(for-each
|
||||
(fn ((key :as string))
|
||||
(let ((val (dict-get (spread-attrs child) key)))
|
||||
(if (= key "class")
|
||||
;; Class: append to existing
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing " " val)
|
||||
val)))
|
||||
(if (= key "style")
|
||||
;; Style: append with semicolon
|
||||
(let ((existing (dom-get-attr el "style")))
|
||||
(dom-set-attr el "style"
|
||||
(cond
|
||||
;; Reactive spread: track signal deps, update attrs on change
|
||||
(and (spread? child) *island-scope*)
|
||||
(reactive-spread el (fn () (render-to-dom arg env new-ns)))
|
||||
;; Static spread: one-shot merge attrs onto parent element
|
||||
(spread? child)
|
||||
(for-each
|
||||
(fn ((key :as string))
|
||||
(let ((val (dict-get (spread-attrs child) key)))
|
||||
(if (= key "class")
|
||||
;; Class: append to existing
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing ";" val)
|
||||
(str existing " " val)
|
||||
val)))
|
||||
;; Other attrs: overwrite
|
||||
(dom-set-attr el key (str val))))))
|
||||
(keys (spread-attrs child)))
|
||||
(if (= key "style")
|
||||
;; Style: append with semicolon
|
||||
(let ((existing (dom-get-attr el "style")))
|
||||
(dom-set-attr el "style"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing ";" val)
|
||||
val)))
|
||||
;; Other attrs: overwrite
|
||||
(dom-set-attr el key (str val))))))
|
||||
(keys (spread-attrs child)))
|
||||
;; Normal child: append to element
|
||||
(dom-append el child))))
|
||||
:else
|
||||
(dom-append el child))))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
@@ -867,6 +872,64 @@
|
||||
:else
|
||||
(dom-set-attr el attr-name (str val)))))))))
|
||||
|
||||
;; reactive-spread — reactively bind spread attrs to parent element.
|
||||
;; Used when a child of an element produces a spread inside an island.
|
||||
;; Tracks signal deps in the spread expression. When signals change:
|
||||
;; old classes are removed, new ones applied. Non-class attrs (data-tw etc.)
|
||||
;; are overwritten. Flushes newly collected CSS rules to live stylesheet.
|
||||
;;
|
||||
;; Multiple reactive spreads on the same element are safe — each tracks
|
||||
;; its own class contribution and only removes/adds its own tokens.
|
||||
(define reactive-spread :effects [render mutation]
|
||||
(fn (el (render-fn :as lambda))
|
||||
(let ((prev-classes (list))
|
||||
(prev-extra-keys (list)))
|
||||
;; Mark for morph protection
|
||||
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") "")))
|
||||
(dom-set-attr el "data-sx-reactive-attrs"
|
||||
(if (empty? existing) "_spread" (str existing ",_spread"))))
|
||||
(effect (fn ()
|
||||
;; 1. Remove previously applied classes from element's class list
|
||||
(when (not (empty? prev-classes))
|
||||
(let ((current (or (dom-get-attr el "class") ""))
|
||||
(tokens (filter (fn (c) (not (= c ""))) (split current " ")))
|
||||
(kept (filter (fn (c)
|
||||
(not (some (fn (pc) (= pc c)) prev-classes)))
|
||||
tokens)))
|
||||
(if (empty? kept)
|
||||
(dom-remove-attr el "class")
|
||||
(dom-set-attr el "class" (join " " kept)))))
|
||||
;; 2. Remove previously applied extra attrs
|
||||
(for-each (fn (k) (dom-remove-attr el k)) prev-extra-keys)
|
||||
;; 3. Re-evaluate the spread expression (tracks signal deps)
|
||||
(let ((result (render-fn)))
|
||||
(if (spread? result)
|
||||
(let ((attrs (spread-attrs result))
|
||||
(cls-str (or (dict-get attrs "class") ""))
|
||||
(new-classes (filter (fn (c) (not (= c "")))
|
||||
(split cls-str " ")))
|
||||
(extra-keys (filter (fn (k) (not (= k "class")))
|
||||
(keys attrs))))
|
||||
(set! prev-classes new-classes)
|
||||
(set! prev-extra-keys extra-keys)
|
||||
;; Append new classes to element
|
||||
(when (not (empty? new-classes))
|
||||
(let ((current (or (dom-get-attr el "class") "")))
|
||||
(dom-set-attr el "class"
|
||||
(if (and current (not (= current "")))
|
||||
(str current " " cls-str)
|
||||
cls-str))))
|
||||
;; Set extra attrs (data-tw, etc.) — simple overwrite
|
||||
(for-each (fn (k)
|
||||
(dom-set-attr el k (str (dict-get attrs k))))
|
||||
extra-keys)
|
||||
;; Flush any newly collected CSS rules to live stylesheet
|
||||
(flush-cssx-to-dom))
|
||||
;; No longer a spread — clear tracked state
|
||||
(do
|
||||
(set! prev-classes (list))
|
||||
(set! prev-extra-keys (list))))))))))
|
||||
|
||||
;; 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 :effects [render mutation]
|
||||
|
||||
Reference in New Issue
Block a user