fix(csr): write loop locals onto client-rendered for-loop items
A client `data-for` passed its loop locals to hydration in memory but never wrote the `data-wrn-loop-locals` attribute the SSR path writes. Anything that resolves locals by READING the DOM -- notably a component's `data-wrn-out-*` output binding, which calls `decodeLoopLocals(componentRoot)` -- therefore found nothing and silently dropped the call, with no console error. A plain DOM handler kept working, because it receives locals through the hydration closure instead, which is what made the failure look arbitrary: the same loop variable resolved for `@click` and vanished for a component output. Both loop paths write the marker now, keyed and non-keyed, so the DOM is the single source of truth. Encoding goes through UTF-8 before base64 as the server's does; `btoa` on a raw string throws above U+00FF, which would take the whole loop down for an ordinary non-ASCII label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -980,6 +980,43 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
renderers[index]();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Write loop locals onto a client-rendered item, mirroring the
|
||||
* data-wrn-loop-locals attribute the server emits for an each block.
|
||||
*
|
||||
* Client data-for used to pass locals to hydration in memory only, so
|
||||
* anything that resolves them by READING the DOM -- a component output
|
||||
* binding calls decodeLoopLocals(componentRoot) -- found nothing and
|
||||
* silently dropped the value. A plain DOM handler kept working, because it
|
||||
* receives locals through the hydration closure, which is what made the
|
||||
* failure look arbitrary. The DOM is the single source of truth for
|
||||
* locals; both paths write it now.
|
||||
*/
|
||||
function writeLoopLocals(node, locals) {
|
||||
if (!node || node.nodeType !== 1 || !locals) return;
|
||||
|
||||
try {
|
||||
var json = JSON.stringify(locals);
|
||||
if (json === undefined) return;
|
||||
|
||||
var bytes = new TextEncoder().encode(json);
|
||||
var binary = "";
|
||||
|
||||
for (var index = 0; index < bytes.length; index++) {
|
||||
binary += String.fromCharCode(bytes[index]);
|
||||
}
|
||||
|
||||
node.setAttribute("data-wrn-loop-locals", window.btoa(binary));
|
||||
} catch (error) {
|
||||
/*
|
||||
* A value that will not serialise (a cycle, a DOM node) must not take
|
||||
* the whole loop down -- the item still renders, and a handler naming
|
||||
* that local fails on its own terms rather than silently.
|
||||
*/
|
||||
console.error("[wrnexus] failed to encode loop locals", error);
|
||||
}
|
||||
}
|
||||
|
||||
function decodeLoopLocals(node) {
|
||||
if (!node) {
|
||||
return {};
|
||||
@@ -2216,6 +2253,8 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
itemIndex;
|
||||
}
|
||||
|
||||
writeLoopLocals(clone, locals);
|
||||
|
||||
hydrateItem(
|
||||
clone,
|
||||
locals,
|
||||
@@ -2292,6 +2331,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
var keyedClone = template.cloneNode(true);
|
||||
writeLoopLocals(keyedClone, keyedLocals);
|
||||
hydrateItem(keyedClone, keyedLocals);
|
||||
record = {
|
||||
node: keyedClone,
|
||||
|
||||
Reference in New Issue
Block a user