@takazudo/zfb 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/css.d.ts +45 -0
- package/dist/css.js +137 -0
- package/dist/css.js.map +1 -0
- package/dist/zudo-react/dom-bindings.js +11 -5
- package/dist/zudo-react/dom-bindings.js.map +1 -1
- package/dist/zudo-react/error-boundary-client.d.ts +3 -0
- package/dist/zudo-react/error-boundary-client.js +232 -0
- package/dist/zudo-react/error-boundary-client.js.map +1 -0
- package/dist/zudo-react/error-boundary-type.d.ts +8 -0
- package/dist/zudo-react/error-boundary-type.js +25 -0
- package/dist/zudo-react/error-boundary-type.js.map +1 -0
- package/dist/zudo-react/error-boundary.d.ts +8 -0
- package/dist/zudo-react/error-boundary.js +13 -0
- package/dist/zudo-react/error-boundary.js.map +1 -0
- package/dist/zudo-react/form-recovery.d.ts +2 -0
- package/dist/zudo-react/form-recovery.js +116 -0
- package/dist/zudo-react/form-recovery.js.map +1 -0
- package/dist/zudo-react/forms.d.ts +38 -4
- package/dist/zudo-react/forms.js +143 -172
- package/dist/zudo-react/forms.js.map +1 -1
- package/dist/zudo-react/hydrate.d.ts +68 -0
- package/dist/zudo-react/hydrate.js +177 -89
- package/dist/zudo-react/hydrate.js.map +1 -1
- package/dist/zudo-react/index.d.ts +1 -0
- package/dist/zudo-react/index.js +1 -0
- package/dist/zudo-react/index.js.map +1 -1
- package/dist/zudo-react/render-html.js +55 -10
- package/dist/zudo-react/render-html.js.map +1 -1
- package/dist/zudo-react/scope.d.ts +21 -13
- package/dist/zudo-react/scope.js +141 -113
- package/dist/zudo-react/scope.js.map +1 -1
- package/dist/zudo-react/style.d.ts +0 -1
- package/dist/zudo-react/style.js +11 -503
- package/dist/zudo-react/style.js.map +1 -1
- package/dist/zudo-react/vocabulary.js +12 -49
- package/dist/zudo-react/vocabulary.js.map +1 -1
- package/package.json +11 -7
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function within(scope, parents) {
|
|
2
|
+
for (let current = scope; current; current = current.parent)
|
|
3
|
+
if (parents.includes(current))
|
|
4
|
+
return true;
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
// Optional policy uses the owning renderer's ledger and errors across package copies.
|
|
8
|
+
export function createFormRecovery(container, host) {
|
|
9
|
+
const { controlOwners, error, batch } = host;
|
|
10
|
+
return {
|
|
11
|
+
prepare(plans, map, mode, replacing, parent) {
|
|
12
|
+
const installed = [...(controlOwners.get(container.ownerDocument) ?? [])].filter((entry) => entry.root === container && entry.scope.active && !within(entry.scope, replacing));
|
|
13
|
+
// Fresh boundary controls still sit in the provisional tree during preflight.
|
|
14
|
+
// Resolve their ancestor form through the hydration map for group identity.
|
|
15
|
+
const plannedForms = new Map();
|
|
16
|
+
for (const plan of plans) {
|
|
17
|
+
const node = map.get(plan.element);
|
|
18
|
+
if (node.localName === "form" && node.id && !plannedForms.has(node.id))
|
|
19
|
+
plannedForms.set(node.id, node);
|
|
20
|
+
}
|
|
21
|
+
const form = (node) => {
|
|
22
|
+
const id = node.getAttribute("form");
|
|
23
|
+
if (id !== null) {
|
|
24
|
+
const target = plannedForms.get(id) ?? container.ownerDocument.getElementById(id);
|
|
25
|
+
return target instanceof Element && target.localName === "form" ? target : null;
|
|
26
|
+
}
|
|
27
|
+
const ancestor = node.closest("form");
|
|
28
|
+
if (ancestor)
|
|
29
|
+
return map.get(ancestor) ?? ancestor;
|
|
30
|
+
return parent instanceof Element
|
|
31
|
+
? parent.localName === "form"
|
|
32
|
+
? parent
|
|
33
|
+
: parent.closest("form")
|
|
34
|
+
: null;
|
|
35
|
+
};
|
|
36
|
+
return { installed, form };
|
|
37
|
+
},
|
|
38
|
+
validate(groupByModel, mode, { installed, form }) {
|
|
39
|
+
for (const [model, group] of groupByModel) {
|
|
40
|
+
for (const binding of group) {
|
|
41
|
+
const radio = binding.node;
|
|
42
|
+
if (binding.kind === "radio" &&
|
|
43
|
+
installed.some((entry) => entry.binding.kind === "radio" &&
|
|
44
|
+
entry.name === radio.name &&
|
|
45
|
+
entry.form === form(radio) &&
|
|
46
|
+
entry.binding.model !== model))
|
|
47
|
+
error("ZR_MODEL_CONFLICT", binding.path, "same-name radios use different models");
|
|
48
|
+
}
|
|
49
|
+
const related = installed
|
|
50
|
+
.filter((entry) => entry.binding.model === model)
|
|
51
|
+
.map((entry) => entry.binding);
|
|
52
|
+
const complete = [...group, ...related];
|
|
53
|
+
for (const binding of group)
|
|
54
|
+
binding.group = complete;
|
|
55
|
+
const radios = complete.every((binding) => binding.kind === "radio");
|
|
56
|
+
if (complete.some((binding) => binding.kind === "radio") && !radios)
|
|
57
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "radio and non-radio share model");
|
|
58
|
+
if (radios) {
|
|
59
|
+
const nodes = complete.map((binding) => binding.node);
|
|
60
|
+
const names = new Set(nodes.map((node) => `${node.name}\u0000${node.getAttribute("form") ?? ""}`));
|
|
61
|
+
const optionValues = nodes.map((node) => node.value);
|
|
62
|
+
if (names.size !== 1 ||
|
|
63
|
+
new Set(nodes.map(form)).size !== 1 ||
|
|
64
|
+
new Set(optionValues).size !== optionValues.length)
|
|
65
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "radio group membership or duplicate value");
|
|
66
|
+
if ((mode === "mount" || group.every((binding) => binding.fresh)) &&
|
|
67
|
+
model.value !== null &&
|
|
68
|
+
!optionValues.includes(model.value))
|
|
69
|
+
error("ZR_MODEL_VALUE", group[0].path, "radio model has no option");
|
|
70
|
+
if (mode === "hydrate" &&
|
|
71
|
+
group.filter((binding) => !binding.fresh && binding.node.checked)
|
|
72
|
+
.length > 1)
|
|
73
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "multiple checked radios");
|
|
74
|
+
}
|
|
75
|
+
else if (mode === "hydrate" &&
|
|
76
|
+
group
|
|
77
|
+
.filter((binding) => !binding.fresh)
|
|
78
|
+
.some((binding, _, adopted) => !Object.is(binding.read(), adopted[0].read())))
|
|
79
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "shared model controls disagree");
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
members(binding) {
|
|
83
|
+
return binding.live
|
|
84
|
+
? [...(controlOwners.get(container.ownerDocument) ?? [])]
|
|
85
|
+
.filter((entry) => entry.root === container &&
|
|
86
|
+
entry.scope.active &&
|
|
87
|
+
entry.binding.model === binding.model)
|
|
88
|
+
.map((entry) => entry.binding)
|
|
89
|
+
: binding.group;
|
|
90
|
+
},
|
|
91
|
+
reconcile(groups, mode) {
|
|
92
|
+
if (mode === "hydrate")
|
|
93
|
+
batch(() => {
|
|
94
|
+
for (const [model, group] of groups) {
|
|
95
|
+
const adopted = group.filter((binding) => !binding.fresh);
|
|
96
|
+
if (adopted.length)
|
|
97
|
+
model.value =
|
|
98
|
+
adopted[0].kind === "radio"
|
|
99
|
+
? (adopted.find((binding) => binding.read() !== null)?.read() ??
|
|
100
|
+
(group.some((binding) => binding.fresh && binding.node.value === model.value)
|
|
101
|
+
? model.value
|
|
102
|
+
: null))
|
|
103
|
+
: adopted[0].read();
|
|
104
|
+
for (const binding of group)
|
|
105
|
+
if (binding.fresh)
|
|
106
|
+
binding.write(model.value);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
else
|
|
110
|
+
for (const [model, group] of groups)
|
|
111
|
+
for (const binding of group)
|
|
112
|
+
binding.write(model.value);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=form-recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-recovery.js","sourceRoot":"","sources":["../../src/zudo-react/form-recovery.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FormRecovery, FormHost } from \"./forms.js\";\nimport type { RuntimeScope } from \"./scope.js\";\nfunction within(scope: RuntimeScope, parents: readonly RuntimeScope[]): boolean {\n for (let current: RuntimeScope | undefined = scope; current; current = current.parent)\n if (parents.includes(current)) return true;\n return false;\n}\n\n// Optional policy uses the owning renderer's ledger and errors across package copies.\nexport function createFormRecovery(container: Element, host: FormHost): FormRecovery {\n const { controlOwners, error, batch } = host;\n return {\n prepare(plans, map, mode, replacing, parent) {\n const installed = [...(controlOwners.get(container.ownerDocument) ?? [])].filter(\n (entry) =>\n entry.root === container && entry.scope.active && !within(entry.scope, replacing),\n );\n // Fresh boundary controls still sit in the provisional tree during preflight.\n // Resolve their ancestor form through the hydration map for group identity.\n const plannedForms = new Map<string, Node>();\n for (const plan of plans) {\n const node = map.get(plan.element) as Element;\n if (node.localName === \"form\" && node.id && !plannedForms.has(node.id))\n plannedForms.set(node.id, node);\n }\n const form = (node: HTMLInputElement): Node | null => {\n const id = node.getAttribute(\"form\");\n if (id !== null) {\n const target = plannedForms.get(id) ?? container.ownerDocument.getElementById(id);\n return target instanceof Element && target.localName === \"form\" ? target : null;\n }\n const ancestor = node.closest(\"form\");\n if (ancestor) return map.get(ancestor) ?? ancestor;\n return parent instanceof Element\n ? parent.localName === \"form\"\n ? parent\n : parent.closest(\"form\")\n : null;\n };\n return { installed, form };\n },\n validate(groupByModel, mode, { installed, form }) {\n for (const [model, group] of groupByModel) {\n for (const binding of group) {\n const radio = binding.node as HTMLInputElement;\n if (\n binding.kind === \"radio\" &&\n installed.some(\n (entry) =>\n entry.binding.kind === \"radio\" &&\n entry.name === radio.name &&\n entry.form === form(radio) &&\n entry.binding.model !== model,\n )\n )\n error(\"ZR_MODEL_CONFLICT\", binding.path, \"same-name radios use different models\");\n }\n const related = installed\n .filter((entry) => entry.binding.model === model)\n .map((entry) => entry.binding);\n const complete = [...group, ...related];\n for (const binding of group) binding.group = complete;\n const radios = complete.every((binding) => binding.kind === \"radio\");\n if (complete.some((binding) => binding.kind === \"radio\") && !radios)\n error(\"ZR_MODEL_CONFLICT\", group[0]!.path, \"radio and non-radio share model\");\n if (radios) {\n const nodes = complete.map((binding) => binding.node as HTMLInputElement);\n const names = new Set(\n nodes.map((node) => `${node.name}\\u0000${node.getAttribute(\"form\") ?? \"\"}`),\n );\n const optionValues = nodes.map((node) => node.value);\n if (\n names.size !== 1 ||\n new Set(nodes.map(form)).size !== 1 ||\n new Set(optionValues).size !== optionValues.length\n )\n error(\"ZR_MODEL_CONFLICT\", group[0]!.path, \"radio group membership or duplicate value\");\n if (\n (mode === \"mount\" || group.every((binding) => binding.fresh)) &&\n model.value !== null &&\n !optionValues.includes(model.value as string)\n )\n error(\"ZR_MODEL_VALUE\", group[0]!.path, \"radio model has no option\");\n if (\n mode === \"hydrate\" &&\n group.filter((binding) => !binding.fresh && (binding.node as HTMLInputElement).checked)\n .length > 1\n )\n error(\"ZR_MODEL_CONFLICT\", group[0]!.path, \"multiple checked radios\");\n } else if (\n mode === \"hydrate\" &&\n group\n .filter((binding) => !binding.fresh)\n .some((binding, _, adopted) => !Object.is(binding.read(), adopted[0]!.read()))\n )\n error(\"ZR_MODEL_CONFLICT\", group[0]!.path, \"shared model controls disagree\");\n }\n },\n members(binding) {\n return binding.live\n ? [...(controlOwners.get(container.ownerDocument) ?? [])]\n .filter(\n (entry) =>\n entry.root === container &&\n entry.scope.active &&\n entry.binding.model === binding.model,\n )\n .map((entry) => entry.binding)\n : binding.group;\n },\n reconcile(groups, mode) {\n if (mode === \"hydrate\")\n batch(() => {\n for (const [model, group] of groups) {\n const adopted = group.filter((binding) => !binding.fresh);\n if (adopted.length)\n model.value =\n adopted[0]!.kind === \"radio\"\n ? (adopted.find((binding) => binding.read() !== null)?.read() ??\n (group.some(\n (binding) =>\n binding.fresh && (binding.node as HTMLInputElement).value === model.value,\n )\n ? model.value\n : null))\n : adopted[0]!.read();\n for (const binding of group) if (binding.fresh) binding.write(model.value);\n }\n });\n else\n for (const [model, group] of groups)\n for (const binding of group) binding.write(model.value);\n },\n };\n}\n"]}
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { batch } from "./scheduler.js";
|
|
1
2
|
import type { Signal } from "./reactive-types.js";
|
|
2
3
|
import type { RuntimeScope } from "./scope.js";
|
|
3
4
|
import { type RootOptions } from "./root.js";
|
|
5
|
+
declare const controlOwners: WeakMap<Document, Set<ControlOwner>>;
|
|
6
|
+
export interface ControlOwner {
|
|
7
|
+
root: Element;
|
|
8
|
+
name: string;
|
|
9
|
+
form: HTMLFormElement | null;
|
|
10
|
+
binding: Binding;
|
|
11
|
+
scope: RuntimeScope;
|
|
12
|
+
}
|
|
4
13
|
type Control = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
5
|
-
type Model = Signal<string | boolean | null>;
|
|
14
|
+
export type Model = Signal<string | boolean | null>;
|
|
6
15
|
export interface FormPlan {
|
|
7
16
|
element: Element;
|
|
8
17
|
props: Readonly<Record<string, unknown>>;
|
|
9
18
|
path: string;
|
|
10
19
|
}
|
|
11
|
-
interface Binding {
|
|
20
|
+
export interface Binding {
|
|
12
21
|
node: Control;
|
|
13
22
|
model: Model;
|
|
23
|
+
group?: Binding[];
|
|
24
|
+
fresh: boolean;
|
|
25
|
+
live: boolean;
|
|
14
26
|
kind: "text" | "textarea" | "checkbox" | "select" | "radio";
|
|
15
27
|
read(): string | boolean | null;
|
|
16
28
|
write(value: string | boolean | null): void;
|
|
@@ -23,7 +35,29 @@ export declare class FormError extends TypeError {
|
|
|
23
35
|
readonly detail: string;
|
|
24
36
|
constructor(code: string, path: string, detail: string);
|
|
25
37
|
}
|
|
26
|
-
|
|
27
|
-
export declare function
|
|
38
|
+
declare function error(code: string, path: string, detail: string): never;
|
|
39
|
+
export declare function prepareForms(plans: FormPlan[], map: Map<Node, Node>, mode: "hydrate" | "mount", container: Element, replacing?: readonly RuntimeScope[], parent?: Node | null): Binding[];
|
|
40
|
+
export declare function reconcileForms(bindings: Binding[], mode: "hydrate" | "mount", container: Element): void;
|
|
28
41
|
export declare function activateForms(bindings: Binding[], scope: RuntimeScope, cleanups: Array<() => void>, options: RootOptions, container: Element): void;
|
|
42
|
+
export interface FormState {
|
|
43
|
+
form(node: HTMLInputElement): Node | null;
|
|
44
|
+
installed: ControlOwner[];
|
|
45
|
+
}
|
|
46
|
+
export interface FormRecovery {
|
|
47
|
+
prepare(plans: FormPlan[], map: Map<Node, Node>, mode: "hydrate" | "mount", replacing: readonly RuntimeScope[], parent?: Node | null): FormState;
|
|
48
|
+
validate(groups: Map<Model, Binding[]>, mode: "hydrate" | "mount", state: FormState): void;
|
|
49
|
+
members(binding: Binding): Binding[] | undefined;
|
|
50
|
+
reconcile(groups: Map<Model, Binding[]>, mode: "hydrate" | "mount"): void;
|
|
51
|
+
}
|
|
52
|
+
export interface FormHost {
|
|
53
|
+
controlOwners: typeof controlOwners;
|
|
54
|
+
error: typeof error;
|
|
55
|
+
batch: typeof batch;
|
|
56
|
+
}
|
|
57
|
+
export declare const formHost: {
|
|
58
|
+
controlOwners: WeakMap<Document, Set<ControlOwner>>;
|
|
59
|
+
error: typeof error;
|
|
60
|
+
batch: typeof batch;
|
|
61
|
+
recoveries: WeakMap<Element, FormRecovery>;
|
|
62
|
+
};
|
|
29
63
|
export {};
|
package/dist/zudo-react/forms.js
CHANGED
|
@@ -3,38 +3,31 @@ import { isReactive } from "./dom-bindings.js";
|
|
|
3
3
|
import { subscribe } from "./reactive.js";
|
|
4
4
|
import { diagnostic, report, rootPath } from "./root.js";
|
|
5
5
|
const compositionKey = Symbol.for("@takazudo/zfb/zudo-react/composition-v1");
|
|
6
|
-
|
|
6
|
+
// Capture every live model before a first boundary can appear in a later render.
|
|
7
|
+
const controlOwners = new WeakMap();
|
|
7
8
|
export class FormError extends TypeError {
|
|
8
|
-
code;
|
|
9
|
-
path;
|
|
10
|
-
detail;
|
|
11
9
|
constructor(code, path, detail) {
|
|
12
10
|
super(`${code}: ${detail} at ${path}`);
|
|
13
|
-
this
|
|
14
|
-
this.path = path;
|
|
15
|
-
this.detail = detail;
|
|
11
|
+
Object.assign(this, { code, path, detail });
|
|
16
12
|
}
|
|
17
13
|
}
|
|
18
14
|
function error(code, path, detail) {
|
|
19
15
|
throw new FormError(code, path, detail);
|
|
20
16
|
}
|
|
21
17
|
function writable(value, path) {
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
error("ZR_MODEL_READONLY", path, "model requires a writable signal");
|
|
35
|
-
return value;
|
|
18
|
+
if (isReactive(value) &&
|
|
19
|
+
"$$zudoWritable" in value &&
|
|
20
|
+
value.$$zudoWritable === "zudo-react.writable.v1") {
|
|
21
|
+
let proto = value;
|
|
22
|
+
while (proto) {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
|
|
24
|
+
if (descriptor) {
|
|
25
|
+
if (typeof descriptor.set === "function")
|
|
26
|
+
return value;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
proto = Object.getPrototypeOf(proto);
|
|
36
30
|
}
|
|
37
|
-
proto = Object.getPrototypeOf(proto);
|
|
38
31
|
}
|
|
39
32
|
return error("ZR_MODEL_READONLY", path, "model requires a writable signal");
|
|
40
33
|
}
|
|
@@ -52,95 +45,50 @@ function kind(node, props) {
|
|
|
52
45
|
return "radio";
|
|
53
46
|
return ["text", "search", "email", "url", "tel", "password"].includes(type) ? "text" : null;
|
|
54
47
|
}
|
|
55
|
-
//
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (typeof value !== "string")
|
|
62
|
-
error("ZR_MODEL_VALUE", path, "text requires string");
|
|
63
|
-
if (node.value !== value)
|
|
64
|
-
node.value = value;
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
// Textarea: read/write string .value; listen to input.
|
|
69
|
-
function textareaAdapter(node, path) {
|
|
70
|
-
return {
|
|
71
|
-
event: "input",
|
|
72
|
-
read: () => node.value,
|
|
73
|
-
write: (value) => {
|
|
74
|
-
if (typeof value !== "string")
|
|
75
|
-
error("ZR_MODEL_VALUE", path, "textarea requires string");
|
|
76
|
-
if (node.value !== value)
|
|
77
|
-
node.value = value;
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// Checkbox: read/write boolean .checked; listen to change.
|
|
82
|
-
function checkboxAdapter(node, path) {
|
|
48
|
+
// All adapters preserve native input/change events and avoid redundant writes.
|
|
49
|
+
function adapterFor(kind, control, path) {
|
|
50
|
+
const node = control;
|
|
51
|
+
const radio = kind === "radio";
|
|
52
|
+
const checkbox = kind === "checkbox";
|
|
53
|
+
const property = checkbox || radio ? "checked" : "value";
|
|
83
54
|
return {
|
|
84
|
-
event: "change",
|
|
85
|
-
read: () => node.checked,
|
|
55
|
+
event: kind === "text" || kind === "textarea" ? "input" : "change",
|
|
56
|
+
read: () => (radio ? (node.checked ? node.value : null) : node[property]),
|
|
86
57
|
write: (value) => {
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
read: () => node.value,
|
|
99
|
-
write: (value) => {
|
|
100
|
-
if (typeof value !== "string")
|
|
101
|
-
error("ZR_MODEL_VALUE", path, "select requires string");
|
|
102
|
-
if (!values(node).includes(value) && !(value === "" && node.selectedIndex === -1))
|
|
103
|
-
error("ZR_MODEL_VALUE", path, `select has no option ${value}`);
|
|
104
|
-
if (node.value !== value)
|
|
105
|
-
node.value = value;
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
// Radio group: read selected option or null; write checked; listen to change.
|
|
110
|
-
function radioAdapter(node) {
|
|
111
|
-
return {
|
|
112
|
-
event: "change",
|
|
113
|
-
read: () => (node.checked ? node.value : null),
|
|
114
|
-
write: (value) => {
|
|
115
|
-
const checked = value === node.value;
|
|
116
|
-
if (node.checked !== checked)
|
|
117
|
-
node.checked = checked;
|
|
58
|
+
if (!radio) {
|
|
59
|
+
if (typeof value !== (checkbox ? "boolean" : "string"))
|
|
60
|
+
error("ZR_MODEL_VALUE", path, `${kind} requires ${checkbox ? "boolean" : "string"}`);
|
|
61
|
+
if (kind === "select" &&
|
|
62
|
+
!values(control).includes(value) &&
|
|
63
|
+
!(value === "" && control.selectedIndex === -1))
|
|
64
|
+
error("ZR_MODEL_VALUE", path, `select has no option ${value}`);
|
|
65
|
+
}
|
|
66
|
+
const next = radio ? value === node.value : value;
|
|
67
|
+
if (node[property] !== next)
|
|
68
|
+
node[property] = next;
|
|
118
69
|
},
|
|
119
70
|
};
|
|
120
71
|
}
|
|
121
|
-
function adapterFor(kind, node, path) {
|
|
122
|
-
switch (kind) {
|
|
123
|
-
case "text":
|
|
124
|
-
return textAdapter(node, path);
|
|
125
|
-
case "textarea":
|
|
126
|
-
return textareaAdapter(node, path);
|
|
127
|
-
case "checkbox":
|
|
128
|
-
return checkboxAdapter(node, path);
|
|
129
|
-
case "select":
|
|
130
|
-
return selectAdapter(node, path);
|
|
131
|
-
case "radio":
|
|
132
|
-
return radioAdapter(node);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
72
|
function values(node) {
|
|
136
73
|
return [...node.options].map((option) => option.value);
|
|
137
74
|
}
|
|
138
75
|
function optionValuesFor(group) {
|
|
139
76
|
return group.map((binding) => binding.node.value);
|
|
140
77
|
}
|
|
141
|
-
|
|
78
|
+
function groupBindings(bindings) {
|
|
79
|
+
const groups = new Map();
|
|
80
|
+
for (const binding of bindings) {
|
|
81
|
+
const group = groups.get(binding.model) ?? [];
|
|
82
|
+
group.push(binding);
|
|
83
|
+
groups.set(binding.model, group);
|
|
84
|
+
}
|
|
85
|
+
return groups;
|
|
86
|
+
}
|
|
87
|
+
export function prepareForms(plans, map, mode, container, replacing = [], parent) {
|
|
88
|
+
const recovery = formRecoveries.get(container);
|
|
89
|
+
const state = recovery?.prepare(plans, map, mode, replacing, parent);
|
|
90
|
+
const form = state?.form ?? ((node) => node.form);
|
|
142
91
|
const bindings = [];
|
|
143
|
-
const groupByModel = new Map();
|
|
144
92
|
const namedRadios = new Map();
|
|
145
93
|
for (const plan of plans) {
|
|
146
94
|
const { props, path } = plan;
|
|
@@ -193,8 +141,11 @@ export function prepareForms(plans, map, mode, container) {
|
|
|
193
141
|
const radio = node;
|
|
194
142
|
if (!radio.name || typeof props.value !== "string")
|
|
195
143
|
error("ZR_MODEL_UNSUPPORTED", path, "radio requires static name and value");
|
|
196
|
-
const owners =
|
|
197
|
-
if ([...(owners ?? [])].some((entry) => entry.root !== container &&
|
|
144
|
+
const owners = controlOwners.get(node.ownerDocument);
|
|
145
|
+
if ([...(owners ?? [])].some((entry) => entry.root !== container &&
|
|
146
|
+
entry.binding.kind === "radio" &&
|
|
147
|
+
entry.name === radio.name &&
|
|
148
|
+
entry.form === form(radio)))
|
|
198
149
|
error("ZR_MODEL_CONFLICT", path, "same-name radio owned by another root");
|
|
199
150
|
const key = `${radio.name}\u0000${radio.getAttribute("form") ?? ""}`;
|
|
200
151
|
const prior = namedRadios.get(key);
|
|
@@ -205,41 +156,50 @@ export function prepareForms(plans, map, mode, container) {
|
|
|
205
156
|
const binding = {
|
|
206
157
|
node: node,
|
|
207
158
|
model,
|
|
159
|
+
fresh: mode === "hydrate" && node === plan.element,
|
|
160
|
+
live: false,
|
|
208
161
|
kind: controlKind,
|
|
209
162
|
path,
|
|
210
163
|
...adapterFor(controlKind, node, path),
|
|
211
164
|
};
|
|
212
165
|
bindings.push(binding);
|
|
213
|
-
const group = groupByModel.get(model) ?? [];
|
|
214
|
-
group.push(binding);
|
|
215
|
-
groupByModel.set(model, group);
|
|
216
166
|
}
|
|
217
167
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
new Set(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
168
|
+
const groupByModel = groupBindings(bindings);
|
|
169
|
+
if (recovery)
|
|
170
|
+
recovery.validate(groupByModel, mode, state);
|
|
171
|
+
else {
|
|
172
|
+
for (const [model, group] of groupByModel) {
|
|
173
|
+
const radios = group.every((binding) => binding.kind === "radio");
|
|
174
|
+
if (group.some((binding) => binding.kind === "radio") && !radios)
|
|
175
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "radio and non-radio share model");
|
|
176
|
+
if (radios) {
|
|
177
|
+
const nodes = group.map((binding) => binding.node);
|
|
178
|
+
const names = new Set(nodes.map((node) => `${node.name}\u0000${node.getAttribute("form") ?? ""}`));
|
|
179
|
+
const optionValues = nodes.map((node) => node.value);
|
|
180
|
+
if (names.size !== 1 ||
|
|
181
|
+
new Set(nodes.map((node) => node.form)).size !== 1 ||
|
|
182
|
+
new Set(optionValues).size !== optionValues.length)
|
|
183
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "radio group membership or duplicate value");
|
|
184
|
+
if (mode === "mount" &&
|
|
185
|
+
model.value !== null &&
|
|
186
|
+
!optionValues.includes(model.value))
|
|
187
|
+
error("ZR_MODEL_VALUE", group[0].path, "radio model has no option");
|
|
188
|
+
if (mode === "hydrate" && nodes.filter((node) => node.checked).length > 1)
|
|
189
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "multiple checked radios");
|
|
190
|
+
}
|
|
191
|
+
else if (mode === "hydrate" &&
|
|
192
|
+
group.some((binding) => !Object.is(binding.read(), group[0].read())))
|
|
193
|
+
error("ZR_MODEL_CONFLICT", group[0].path, "shared model controls disagree");
|
|
234
194
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (radios) {
|
|
195
|
+
}
|
|
196
|
+
for (const group of groupByModel.values()) {
|
|
197
|
+
if (group[0].kind === "radio") {
|
|
239
198
|
for (const binding of group) {
|
|
240
199
|
const originalWrite = binding.write;
|
|
241
200
|
binding.write = (value) => {
|
|
242
|
-
if (value !== null &&
|
|
201
|
+
if (value !== null &&
|
|
202
|
+
!optionValuesFor(formRecoveries.get(container)?.members(binding) ?? group).includes(value))
|
|
243
203
|
error("ZR_MODEL_VALUE", binding.path, "radio model has no option");
|
|
244
204
|
originalWrite(value);
|
|
245
205
|
};
|
|
@@ -248,40 +208,51 @@ export function prepareForms(plans, map, mode, container) {
|
|
|
248
208
|
}
|
|
249
209
|
return bindings;
|
|
250
210
|
}
|
|
251
|
-
export function reconcileForms(bindings, mode) {
|
|
252
|
-
const groups =
|
|
253
|
-
|
|
254
|
-
|
|
211
|
+
export function reconcileForms(bindings, mode, container) {
|
|
212
|
+
const groups = groupBindings(bindings);
|
|
213
|
+
const recovery = formRecoveries.get(container);
|
|
214
|
+
if (recovery)
|
|
215
|
+
return recovery.reconcile(groups, mode);
|
|
255
216
|
if (mode === "hydrate")
|
|
256
|
-
|
|
257
|
-
for (const [model, group] of groups) {
|
|
258
|
-
model.value =
|
|
259
|
-
group[0].kind === "radio"
|
|
260
|
-
? (group.find((binding) => binding.read() !== null)?.read() ?? null)
|
|
261
|
-
: group[0].read();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
217
|
+
readModels(groups);
|
|
264
218
|
else
|
|
265
219
|
for (const [model, group] of groups)
|
|
266
220
|
for (const binding of group)
|
|
267
221
|
binding.write(model.value);
|
|
268
222
|
}
|
|
223
|
+
function readModels(groups) {
|
|
224
|
+
batch(() => {
|
|
225
|
+
for (const [model, group] of groups)
|
|
226
|
+
model.value =
|
|
227
|
+
group[0].kind === "radio"
|
|
228
|
+
? (group.find((binding) => binding.read() !== null)?.read() ?? null)
|
|
229
|
+
: group[0].read();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
269
232
|
export function activateForms(bindings, scope, cleanups, options, container) {
|
|
270
233
|
const byForm = new Map();
|
|
271
|
-
const owned =
|
|
272
|
-
|
|
273
|
-
radioOwners.set(container.ownerDocument, owned);
|
|
234
|
+
const owned = controlOwners.get(container.ownerDocument) ?? new Set();
|
|
235
|
+
controlOwners.set(container.ownerDocument, owned);
|
|
274
236
|
const registrations = [];
|
|
275
|
-
for (const binding of bindings)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
237
|
+
for (const binding of bindings) {
|
|
238
|
+
const node = binding.node;
|
|
239
|
+
const registration = {
|
|
240
|
+
root: container,
|
|
241
|
+
name: node.name,
|
|
242
|
+
form: (node.hasAttribute("form")
|
|
243
|
+
? node.ownerDocument.getElementById(node.getAttribute("form"))
|
|
244
|
+
: node.closest("form")),
|
|
245
|
+
binding,
|
|
246
|
+
scope,
|
|
247
|
+
};
|
|
248
|
+
registrations.push(registration);
|
|
249
|
+
owned.add(registration);
|
|
250
|
+
}
|
|
282
251
|
cleanups.push(() => {
|
|
283
|
-
for (const registration of registrations)
|
|
252
|
+
for (const registration of registrations) {
|
|
253
|
+
registration.binding.live = false;
|
|
284
254
|
owned.delete(registration);
|
|
255
|
+
}
|
|
285
256
|
});
|
|
286
257
|
for (const binding of bindings) {
|
|
287
258
|
const { node, model } = binding;
|
|
@@ -309,20 +280,16 @@ export function activateForms(bindings, scope, cleanups, options, container) {
|
|
|
309
280
|
unknown = false;
|
|
310
281
|
update();
|
|
311
282
|
};
|
|
312
|
-
|
|
313
|
-
if (textual)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
node.addEventListener(
|
|
317
|
-
}
|
|
283
|
+
const listeners = [[binding.event, onInput]];
|
|
284
|
+
if (textual)
|
|
285
|
+
listeners.push(["compositionstart", onStart], ["compositionend", onFinish], ["blur", onFinish]);
|
|
286
|
+
for (const [name, handler] of listeners)
|
|
287
|
+
node.addEventListener(name, handler);
|
|
318
288
|
cleanups.push(() => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
node.removeEventListener("compositionstart", onStart);
|
|
322
|
-
node.removeEventListener("compositionend", onFinish);
|
|
323
|
-
node.removeEventListener("blur", onFinish);
|
|
324
|
-
}
|
|
289
|
+
for (const [name, handler] of listeners)
|
|
290
|
+
node.removeEventListener(name, handler);
|
|
325
291
|
});
|
|
292
|
+
const reportUpdate = (code, actual) => report(options, diagnostic(options, code, "update", `${rootPath(container)}${binding.path}`, "valid model update", actual));
|
|
326
293
|
const subscription = subscribe(() => {
|
|
327
294
|
const value = model.value;
|
|
328
295
|
if (!composing && !unknown) {
|
|
@@ -332,14 +299,15 @@ export function activateForms(bindings, scope, cleanups, options, container) {
|
|
|
332
299
|
catch (failure) {
|
|
333
300
|
if (!(failure instanceof FormError))
|
|
334
301
|
throw failure;
|
|
335
|
-
|
|
302
|
+
reportUpdate(failure.code, failure.detail);
|
|
336
303
|
}
|
|
337
304
|
}
|
|
338
305
|
}, {
|
|
339
306
|
active: () => scope.active,
|
|
340
|
-
report:
|
|
307
|
+
report: reportUpdate,
|
|
341
308
|
});
|
|
342
309
|
subscription.run();
|
|
310
|
+
binding.live = true;
|
|
343
311
|
cleanups.push(() => subscription.dispose());
|
|
344
312
|
const form = node.form;
|
|
345
313
|
if (form)
|
|
@@ -350,16 +318,17 @@ export function activateForms(bindings, scope, cleanups, options, container) {
|
|
|
350
318
|
const onReset = (event) => queueMicrotask(() => {
|
|
351
319
|
if (!active || event.defaultPrevented)
|
|
352
320
|
return;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
321
|
+
// A boundary owns only part of a native radio group. Read all current
|
|
322
|
+
// members after the browser reset, not just this scope's unchecked part.
|
|
323
|
+
// Keep ownership-local listeners so disposing a replaced attempt still
|
|
324
|
+
// cancels its queued reset work.
|
|
325
|
+
const recovery = formRecoveries.get(container);
|
|
326
|
+
const current = recovery
|
|
327
|
+
? [
|
|
328
|
+
...new Set(members.flatMap((binding) => recovery.members(binding) ?? [binding])),
|
|
329
|
+
].filter((binding) => binding.node.form === form)
|
|
330
|
+
: members;
|
|
331
|
+
readModels(groupBindings(current));
|
|
363
332
|
});
|
|
364
333
|
form.addEventListener("reset", onReset);
|
|
365
334
|
cleanups.push(() => {
|
|
@@ -368,4 +337,6 @@ export function activateForms(bindings, scope, cleanups, options, container) {
|
|
|
368
337
|
});
|
|
369
338
|
}
|
|
370
339
|
}
|
|
340
|
+
const formRecoveries = new WeakMap();
|
|
341
|
+
export const formHost = { controlOwners, error, batch, recoveries: formRecoveries };
|
|
371
342
|
//# sourceMappingURL=forms.js.map
|