feat: make state-based attributes reactive

This commit is contained in:
2026-07-14 00:43:07 +05:30
parent 4ce087b238
commit 420706ca3b
61 changed files with 221 additions and 99 deletions
+40 -11
View File
@@ -79,8 +79,35 @@ function eventAttribute(name: string): string {
return `data-on-${name}`;
}
function renderAttrs(attrs: Attr[], csrId?: string): string {
const rendered = attrs.map(renderAttr).join("");
function reactiveAttrValue(raw: string, reactive: PageReactive): string | null {
let found = false;
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
const expr = inner.trim();
if (!exprRefsState(expr, reactive.stateNames)) return whole;
found = true;
try {
const result = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
return result == null ? "" : String(result);
} catch {
return whole;
}
});
return found ? value : null;
}
function renderAttrs(attrs: Attr[], csrId?: string, reactive: PageReactive | null = null): string {
let bindIndex = 0;
const rendered = attrs
.map((attr) => {
const base = renderAttr(attr);
if (!reactive || attr.event || attr.boolean || !base || !attr.value.includes("{"))
return base;
const initial = reactiveAttrValue(attr.value, reactive);
if (initial === null) return base;
const marker = JSON.stringify([attr.name, attr.value]);
return ` ${attr.name}="${attrEscape(initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
})
.join("");
return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered;
}
@@ -312,7 +339,7 @@ function renderNode(
// Void elements (<br>, <img>, …) have no closing tag and no children.
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${renderAttrs(node.attrs, csrId)}>`;
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>`;
}
const inner =
@@ -331,7 +358,7 @@ function renderNode(
)
.join("");
return `<${node.tag}${renderAttrs(node.attrs, csrId)}>${inner}</${node.tag}>`;
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
}
function ssrMarker(bindings: SsrBinding[], binding: RenderBinding): string {
@@ -745,14 +772,16 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
);
}
let bindIndex = 0;
const attrs = node.attrs
.map((a) =>
a.event
? ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"`
: a.boolean
? ` ${a.name}`
: ` ${a.name}="${compileAttrValue(a.value, ctx)}"`,
)
.map((a) => {
if (a.event) return ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"`;
if (a.boolean) return ` ${a.name}`;
const rendered = ` ${a.name}="${compileAttrValue(a.value, ctx)}"`;
if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) return rendered;
const marker = attrEscape(JSON.stringify([a.name, a.value]));
return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
})
.join("");
// A `data-for` element introduces loop variables for its subtree.