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:
2026-03-13 05:16:13 +00:00
parent 846719908f
commit 36b070f796
2 changed files with 125 additions and 24 deletions

View File

@@ -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");