release: WRNexusJS 0.2.46
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.2.45",
|
||||
"version": "0.2.46",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -43,6 +43,34 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
});
|
||||
}
|
||||
|
||||
function decodeScopePayload(value) {
|
||||
var binary = window.atob(value);
|
||||
var bytes =
|
||||
new Uint8Array(binary.length);
|
||||
|
||||
for (
|
||||
var index = 0;
|
||||
index < binary.length;
|
||||
index++
|
||||
) {
|
||||
bytes[index] =
|
||||
binary.charCodeAt(index);
|
||||
}
|
||||
|
||||
var decoded =
|
||||
new TextDecoder(
|
||||
"utf-8",
|
||||
).decode(bytes);
|
||||
|
||||
var parsed =
|
||||
JSON.parse(decoded);
|
||||
|
||||
return parsed &&
|
||||
typeof parsed === "object"
|
||||
? parsed
|
||||
: {};
|
||||
}
|
||||
|
||||
function decodeBehaviorPayload(value) {
|
||||
var binary = window.atob(value);
|
||||
var bytes = new Uint8Array(binary.length);
|
||||
@@ -310,11 +338,43 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function setupScope(el) {
|
||||
if (el.__wrnexusScope) return; // idempotent: safe to call again after an HMR morph
|
||||
if (el.__wrnexusScope) return;
|
||||
|
||||
el.__wrnexusScope = true;
|
||||
el.__wrnexusHydrated = true; // marks the subtree as client-owned for the HMR morph
|
||||
var decl = el.getAttribute("data-scope") || "";
|
||||
var initial = parseScopeDecl(decl);
|
||||
el.__wrnexusHydrated = true;
|
||||
|
||||
var encodedScope =
|
||||
el.getAttribute(
|
||||
"data-wrn-scope",
|
||||
);
|
||||
|
||||
var initial;
|
||||
|
||||
if (encodedScope) {
|
||||
try {
|
||||
initial =
|
||||
decodeScopePayload(
|
||||
encodedScope,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[wrnexus] failed to decode scope payload",
|
||||
error,
|
||||
);
|
||||
|
||||
initial = {};
|
||||
}
|
||||
} else {
|
||||
var declaration =
|
||||
el.getAttribute(
|
||||
"data-scope",
|
||||
) || "";
|
||||
|
||||
initial =
|
||||
parseScopeDecl(
|
||||
declaration,
|
||||
);
|
||||
}
|
||||
|
||||
var signals = {};
|
||||
Object.keys(initial).forEach(function (k) { signals[k] = signal(initial[k]); });
|
||||
@@ -374,11 +434,17 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
binary.charCodeAt(index);
|
||||
}
|
||||
|
||||
return JSON.parse(
|
||||
new TextDecoder(
|
||||
"utf-8",
|
||||
).decode(bytes),
|
||||
);
|
||||
var value =
|
||||
JSON.parse(
|
||||
new TextDecoder(
|
||||
"utf-8",
|
||||
).decode(bytes),
|
||||
);
|
||||
|
||||
return value &&
|
||||
typeof value === "object"
|
||||
? value
|
||||
: {};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[wrnexus] failed to decode loop locals",
|
||||
@@ -572,7 +638,13 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// so an outer scope never clobbers an inner one's values.
|
||||
function owns(node) {
|
||||
var host = node.nodeType === 1 ? node : node.parentNode;
|
||||
return !!host && host.closest && host.closest("[data-scope]") === el;
|
||||
return (
|
||||
!!host &&
|
||||
host.closest &&
|
||||
host.closest(
|
||||
"[data-scope], [data-wrn-scope]",
|
||||
) === el
|
||||
);
|
||||
}
|
||||
|
||||
// --- data-for list rendering -------------------------------------------
|
||||
@@ -925,34 +997,128 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
}
|
||||
|
||||
Array.prototype.slice.call(el.querySelectorAll("[data-for]")).forEach(function (tpl) {
|
||||
if (!tpl.parentNode || !owns(tpl)) return;
|
||||
var spec = parseFor(tpl.getAttribute("data-for"));
|
||||
if (!spec) return;
|
||||
tpl.removeAttribute("data-for");
|
||||
var parent = tpl.parentNode;
|
||||
var marker = document.createComment("wire-for");
|
||||
parent.insertBefore(marker, tpl);
|
||||
Array.prototype.slice
|
||||
.call(
|
||||
el.querySelectorAll(
|
||||
"[data-for]",
|
||||
),
|
||||
)
|
||||
.forEach(function (tpl) {
|
||||
if (
|
||||
!tpl.parentNode ||
|
||||
!owns(tpl)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
var spec =
|
||||
parseFor(
|
||||
tpl.getAttribute(
|
||||
"data-for",
|
||||
),
|
||||
);
|
||||
|
||||
if (!spec) {
|
||||
return;
|
||||
}
|
||||
|
||||
var template =
|
||||
tpl.cloneNode(true);
|
||||
|
||||
template.removeAttribute(
|
||||
"data-for",
|
||||
);
|
||||
|
||||
var parent =
|
||||
tpl.parentNode;
|
||||
|
||||
var marker =
|
||||
document.createComment(
|
||||
"wrn-for",
|
||||
);
|
||||
|
||||
parent.insertBefore(
|
||||
marker,
|
||||
tpl,
|
||||
);
|
||||
|
||||
parent.removeChild(tpl);
|
||||
|
||||
var clones = [];
|
||||
|
||||
reactive(function () {
|
||||
var list = evalExpr(spec.list);
|
||||
if (!list || typeof list.length !== "number") list = [];
|
||||
for (var c = 0; c < clones.length; c++) {
|
||||
if (clones[c].parentNode) clones[c].parentNode.removeChild(clones[c]);
|
||||
var list =
|
||||
evalExpr(spec.list);
|
||||
|
||||
if (!Array.isArray(list)) {
|
||||
console.error(
|
||||
"[wrnexus] data-for expected an array for '" +
|
||||
spec.list +
|
||||
"', received",
|
||||
list,
|
||||
);
|
||||
|
||||
list = [];
|
||||
}
|
||||
|
||||
for (
|
||||
var cloneIndex = 0;
|
||||
cloneIndex <
|
||||
clones.length;
|
||||
cloneIndex++
|
||||
) {
|
||||
var existingClone =
|
||||
clones[cloneIndex];
|
||||
|
||||
if (
|
||||
existingClone.parentNode
|
||||
) {
|
||||
existingClone.parentNode
|
||||
.removeChild(
|
||||
existingClone,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
clones = [];
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var clone = tpl.cloneNode(true);
|
||||
|
||||
var fragment =
|
||||
document.createDocumentFragment();
|
||||
|
||||
for (
|
||||
var itemIndex = 0;
|
||||
itemIndex < list.length;
|
||||
itemIndex++
|
||||
) {
|
||||
var clone =
|
||||
template.cloneNode(true);
|
||||
|
||||
var locals = {};
|
||||
locals[spec.item] = list[i];
|
||||
if (spec.index) locals[spec.index] = i;
|
||||
hydrateItem(clone, locals);
|
||||
frag.appendChild(clone);
|
||||
|
||||
locals[spec.item] =
|
||||
list[itemIndex];
|
||||
|
||||
if (spec.index) {
|
||||
locals[spec.index] =
|
||||
itemIndex;
|
||||
}
|
||||
|
||||
hydrateItem(
|
||||
clone,
|
||||
locals,
|
||||
);
|
||||
|
||||
fragment.appendChild(
|
||||
clone,
|
||||
);
|
||||
|
||||
clones.push(clone);
|
||||
}
|
||||
parent.insertBefore(frag, marker.nextSibling);
|
||||
|
||||
parent.insertBefore(
|
||||
fragment,
|
||||
marker.nextSibling,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1009,11 +1175,14 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
expression,
|
||||
decodeLoopLocals(node),
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
node.classList.toggle(className, enabled);
|
||||
node.classList.toggle(
|
||||
className,
|
||||
enabled,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1076,7 +1245,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
try {
|
||||
raw = evalExpr(
|
||||
exact[1].trim(),
|
||||
locals,
|
||||
decodeLoopLocals(node),
|
||||
);
|
||||
} catch (error) {
|
||||
return;
|
||||
@@ -1090,10 +1259,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
) {
|
||||
try {
|
||||
var value =
|
||||
evalExpr(
|
||||
expression.trim(),
|
||||
locals,
|
||||
);
|
||||
evalExpr(
|
||||
expression.trim(),
|
||||
decodeLoopLocals(node),
|
||||
);
|
||||
|
||||
return value == null
|
||||
? ""
|
||||
@@ -1125,7 +1294,18 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
reactive(function () {
|
||||
node.nodeValue = tpl.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, doubleExpr, singleExpr) {
|
||||
var expr = doubleExpr || singleExpr;
|
||||
try { return String(evalExpr(expr.trim())); } catch (err) { return ""; }
|
||||
try {
|
||||
var value = evalExpr(
|
||||
expr.trim(),
|
||||
decodeLoopLocals(
|
||||
node.parentNode,
|
||||
),
|
||||
);
|
||||
|
||||
return value == null
|
||||
? ""
|
||||
: String(value);
|
||||
} catch (err) { return ""; }
|
||||
});
|
||||
});
|
||||
})(textNode, template);
|
||||
@@ -1152,16 +1332,24 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
var stmt = attr.value;
|
||||
var listener = function (event) {
|
||||
try {
|
||||
var locals =
|
||||
decodeLoopLocals(node);
|
||||
var locals =
|
||||
decodeLoopLocals(node);
|
||||
|
||||
locals.event = event;
|
||||
locals.$event = event;
|
||||
locals.event = event;
|
||||
locals.$event = event;
|
||||
|
||||
runStmt(stmt, locals);
|
||||
try {
|
||||
runStmt(
|
||||
stmt,
|
||||
locals,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[wrnexus] handler error in '" + stmt + "'", error);
|
||||
console.error(
|
||||
"[wrnexus] handler error in '" +
|
||||
stmt +
|
||||
"'",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1276,8 +1464,24 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
function hydrateScopes(root) {
|
||||
var host = root || document;
|
||||
if (host.nodeType === 1 && host.matches && host.matches("[data-scope]")) setupScope(host);
|
||||
if (host.querySelectorAll) host.querySelectorAll("[data-scope]").forEach(setupScope);
|
||||
|
||||
var scopeSelector =
|
||||
"[data-scope], [data-wrn-scope]";
|
||||
|
||||
if (
|
||||
host.nodeType === 1 &&
|
||||
host.matches &&
|
||||
host.matches(scopeSelector)
|
||||
) {
|
||||
setupScope(host);
|
||||
}
|
||||
|
||||
if (host.querySelectorAll) {
|
||||
host
|
||||
.querySelectorAll(scopeSelector)
|
||||
.forEach(setupScope);
|
||||
}
|
||||
|
||||
ensureBehaviorObserver();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect, beforeEach } from "bun:test";
|
||||
import { Window } from "happy-dom";
|
||||
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
|
||||
import { mountHtml } from "@wrnexus/test";
|
||||
|
||||
// Fresh DOM per test, with the runtime's globals bound.
|
||||
function mount(html: string): Window {
|
||||
@@ -164,3 +165,48 @@ test("independent signals in one scope update correctly (dependency tracking)",
|
||||
click("bb");
|
||||
expect([ta(), tb()]).toEqual(["2", "101"]);
|
||||
});
|
||||
test("data-for preserves arrays of objects from encoded component scope", () => {
|
||||
const items = [
|
||||
{
|
||||
question: "One",
|
||||
answer: "First",
|
||||
},
|
||||
{
|
||||
question: "Two",
|
||||
answer: "Second",
|
||||
},
|
||||
{
|
||||
question: "Three",
|
||||
answer: "Third",
|
||||
},
|
||||
{
|
||||
question: "Four",
|
||||
answer: "Fourth",
|
||||
},
|
||||
];
|
||||
|
||||
const encoded = Buffer.from(
|
||||
JSON.stringify({
|
||||
items,
|
||||
}),
|
||||
"utf8",
|
||||
).toString("base64");
|
||||
|
||||
const dom = mountHtml(`
|
||||
<div data-wrn-scope="${encoded}">
|
||||
<article data-for="item, index in items">
|
||||
<span>{index + 1}</span>
|
||||
<strong>{item.question}</strong>
|
||||
</article>
|
||||
</div>
|
||||
`);
|
||||
|
||||
expect(dom.querySelectorAll("article")).toHaveLength(4);
|
||||
|
||||
expect(dom.querySelectorAll("strong").map((element) => element.textContent)).toEqual([
|
||||
"One",
|
||||
"Two",
|
||||
"Three",
|
||||
"Four",
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user