Make if and each blocks reactive on the client
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-19 00:22:00 +05:30
parent c0c2fa4595
commit f199385204
8 changed files with 330 additions and 27 deletions
+118 -5
View File
@@ -593,7 +593,22 @@ function renderNode(
// templateEscape, swapped for its real `${…}` code after escaping.
if (node.type === "each" || node.type === "if") {
loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
return `\x00WRNEACH${loops.length - 1}\x00`;
const definition =
node.type === "each"
? {
list: node.list,
item: node.item,
index: node.index,
key: node.key,
body: renderClientControlTemplate(node.body),
empty: renderClientControlTemplate(node.empty),
}
: node.branches.map((branch) => ({
cond: branch.cond,
body: renderClientControlTemplate(branch.body),
}));
const attribute = node.type === "each" ? "data-wrn-each" : "data-wrn-if";
return `<template ${attribute}="${encodeClientControl(definition)}"></template>\x00WRNEACH${loops.length - 1}\x00<template data-wrn-control-end></template>`;
}
if (node.tag === "Static" || node.tag === "Dynamic") {
@@ -2037,6 +2052,87 @@ function compileAttrValue(raw: string, ctx: CompCtx): string {
return out + escLit(attrEscape(raw.slice(last)));
}
/**
* Serialize a control-block body as inert browser-side template markup.
* Values deliberately remain as mustaches: the CSR runtime evaluates them
* against the component scope (and `{#each}` locals) when it materializes the
* template. The string is base64 encoded before it is placed in HTML.
*/
function renderClientControlTemplate(nodes: ViewNode[]): string {
const render = (node: ViewNode): string => {
if (node.type === "text") {
return node.value.replace(/\{([^{}]+)\}/g, (whole, rawExpression: string) => {
const expression = rawExpression.trim();
return expression.startsWith("t:")
? `<span data-t="${attrEscape(expression.slice(2).trim())}"></span>`
: `<span data-text="${attrEscape(expression)}">${whole}</span>`;
});
}
if (node.type === "each") {
return `<template data-wrn-each="${attrEscape(
encodeClientControl({
list: node.list,
item: node.item,
index: node.index,
key: node.key,
body: renderClientControlTemplate(node.body),
empty: renderClientControlTemplate(node.empty),
}),
)}"></template><template data-wrn-control-end></template>`;
}
if (node.type === "if") {
return `<template data-wrn-if="${attrEscape(
encodeClientControl(
node.branches.map((branch) => ({
cond: branch.cond,
body: renderClientControlTemplate(branch.body),
})),
),
)}"></template><template data-wrn-control-end></template>`;
}
const componentTag = isComponentTag(node.tag);
let bindIndex = 0;
const attrs = node.attrs
.map((attribute) => {
const name = attribute.event
? componentTag
? componentEventAttribute(attribute.name)
: eventAttribute(attribute.name)
: attribute.name;
if (attribute.boolean) return ` ${name}`;
if (attribute.name.startsWith("class:")) {
const expression = unwrapDirectiveExpression(attribute.value);
return ` data-wrn-class-${bindIndex++}="${attrEscape(
JSON.stringify([attribute.name.slice("class:".length), expression]),
)}"`;
}
if (attribute.name === "data-show") {
return ` data-show="${attrEscape(unwrapDirectiveExpression(attribute.value))}"`;
}
const rendered = ` ${name}="${attrEscape(attribute.value)}"`;
return attribute.value.includes("{")
? `${rendered} data-wrn-bind-${bindIndex++}="${attrEscape(
JSON.stringify([name, attribute.value]),
)}"`
: rendered;
})
.join("");
const children = node.children.map(render).join("");
if (node.tag === "Static") return children;
if (componentTag)
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${children}</div>`;
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) return `<${node.tag}${attrs}>`;
return `<${node.tag}${attrs}>${children}</${node.tag}>`;
};
return nodes.map(render).join("");
}
function encodeClientControl(value: unknown): string {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
}
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
let expression = "``";
@@ -2051,7 +2147,14 @@ function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
}
return "${" + expression + "}";
const definition = encodeClientControl(
node.branches.map((branch) => ({
cond: branch.cond,
body: renderClientControlTemplate(branch.body),
})),
);
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
}
function renderComponentEachNode(node: EachNode, ctx: CompCtx): string {
@@ -2067,7 +2170,7 @@ function renderComponentEachNode(node: EachNode, ctx: CompCtx): string {
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
return (
const serverBody =
"${(() => { const __wl = Array.isArray(" +
list +
") ? (" +
@@ -2080,8 +2183,18 @@ function renderComponentEachNode(node: EachNode, ctx: CompCtx): string {
body +
'`).join("") : `' +
empty +
"`; })()}"
);
"`; })()}";
const definition = encodeClientControl({
list: node.list,
item: node.item,
index: node.index,
key: node.key,
body: renderClientControlTemplate(node.body),
empty: renderClientControlTemplate(node.empty),
});
return `<template data-wrn-each="${definition}"></template>${serverBody}<template data-wrn-control-end></template>`;
}
function serverLoopLocalsAttribute(ctx: CompCtx): string {