feat(framework): make component props reactive
This commit is contained in:
@@ -2600,9 +2600,43 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
});
|
||||
});
|
||||
|
||||
if (behavior) {
|
||||
installBehaviorFunctions(behavior.functions);
|
||||
var publicScopeApi = {
|
||||
// Prop expressions belong to the parent that mounted the component. The
|
||||
// server forwards these markers onto the rendered child root; evaluate
|
||||
// them here and write changes into the child's prop signals.
|
||||
Array.prototype.slice
|
||||
.call(el.querySelectorAll("[data-scope], [data-wrn-scope]"))
|
||||
.forEach(function (node) {
|
||||
if (!node.parentNode || ownerScope(node.parentNode) !== el) return;
|
||||
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
|
||||
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
|
||||
var binding;
|
||||
try { binding = JSON.parse(attr.value); } catch (_) { return; }
|
||||
if (!binding || binding.length !== 2) return;
|
||||
var propName = binding[0];
|
||||
var template = binding[1];
|
||||
reactive(function () {
|
||||
var exact = /^\{([^{}]+)\}$/.exec(template);
|
||||
var value;
|
||||
try {
|
||||
value = exact
|
||||
? evalExpr(exact[1].trim(), decodeLoopLocals(node))
|
||||
: template.replace(/\{([^{}]+)\}/g, function (_, expression) {
|
||||
var part = evalExpr(expression.trim(), decodeLoopLocals(node));
|
||||
return part == null ? "" : String(part);
|
||||
});
|
||||
} catch (_) { return; }
|
||||
var apply = function () {
|
||||
if (node.__wrnexusScopeApi) node.__wrnexusScopeApi.set(propName, value);
|
||||
};
|
||||
if (node.__wrnexusScopeApi) apply();
|
||||
else queueMicrotask(apply);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Every hydrated scope exposes state writes. Prop-only components need the
|
||||
// same API even when they have no behavior block.
|
||||
var publicScopeApi = {
|
||||
get: peekScope,
|
||||
set: writeScope,
|
||||
call: function (name) {
|
||||
@@ -2610,11 +2644,14 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (typeof fn !== "function") return undefined;
|
||||
return fn.apply(null, Array.prototype.slice.call(arguments, 1));
|
||||
},
|
||||
};
|
||||
el.__wrnexusScopeApi = publicScopeApi;
|
||||
el.querySelectorAll("[data-wrn-select]").forEach(function (select) {
|
||||
select.__wrnexusScopeApi = publicScopeApi;
|
||||
});
|
||||
};
|
||||
el.__wrnexusScopeApi = publicScopeApi;
|
||||
el.querySelectorAll("[data-wrn-select]").forEach(function (select) {
|
||||
select.__wrnexusScopeApi = publicScopeApi;
|
||||
});
|
||||
|
||||
if (behavior) {
|
||||
installBehaviorFunctions(behavior.functions);
|
||||
|
||||
(behavior.effects || []).forEach(function (source) {
|
||||
if (typeof source !== "string" || !source.trim()) return;
|
||||
|
||||
@@ -362,6 +362,20 @@ test("reactive attribute bindings update input and accessibility attributes", ()
|
||||
expect(button.getAttribute("aria-label")).toBe("Hide password");
|
||||
});
|
||||
|
||||
test("parent state updates a mounted child's reactive prop", async () => {
|
||||
const win = mount(
|
||||
`<div data-scope="n: 1">` +
|
||||
`<button data-on-click="n++">increment</button>` +
|
||||
`<div data-scope="value: 1" data-wrn-prop-bind-0='["value","{n}"]'>` +
|
||||
`<span id="child-value" data-text="value">1</span>` +
|
||||
`</div></div>`,
|
||||
);
|
||||
await Promise.resolve();
|
||||
expect(win.document.querySelector("#child-value")?.textContent).toBe("1");
|
||||
(win.document.querySelector("button") as unknown as HTMLElement).click();
|
||||
expect(win.document.querySelector("#child-value")?.textContent).toBe("2");
|
||||
});
|
||||
|
||||
test("independent signals in one scope update correctly (dependency tracking)", () => {
|
||||
const win = mount(
|
||||
`<div data-scope="a: 0, b: 100">
|
||||
|
||||
Reference in New Issue
Block a user