Make if and each blocks reactive on the client
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user