@native-form/core 0.1.0 → 0.1.2
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 +6 -3
- package/dist/index.cjs +2097 -0
- package/dist/index.d.cts +517 -0
- package/dist/index.d.ts +517 -9
- package/dist/index.js +2054 -7
- package/package.json +5 -3
- package/dist/Discovery.d.ts +0 -2
- package/dist/Discovery.d.ts.map +0 -1
- package/dist/Discovery.js +0 -2
- package/dist/Discovery.js.map +0 -1
- package/dist/FieldErrorRenderer.d.ts +0 -2
- package/dist/FieldErrorRenderer.d.ts.map +0 -1
- package/dist/FieldErrorRenderer.js +0 -2
- package/dist/FieldErrorRenderer.js.map +0 -1
- package/dist/FieldNode.d.ts +0 -2
- package/dist/FieldNode.d.ts.map +0 -1
- package/dist/FieldNode.js +0 -2
- package/dist/FieldNode.js.map +0 -1
- package/dist/FieldRegistry.d.ts +0 -2
- package/dist/FieldRegistry.d.ts.map +0 -1
- package/dist/FieldRegistry.js +0 -2
- package/dist/FieldRegistry.js.map +0 -1
- package/dist/FieldState.d.ts +0 -2
- package/dist/FieldState.d.ts.map +0 -1
- package/dist/FieldState.js +0 -2
- package/dist/FieldState.js.map +0 -1
- package/dist/Path.d.ts +0 -78
- package/dist/Path.d.ts.map +0 -1
- package/dist/Path.js +0 -219
- package/dist/Path.js.map +0 -1
- package/dist/Runtime.d.ts +0 -2
- package/dist/Runtime.d.ts.map +0 -1
- package/dist/Runtime.js +0 -2
- package/dist/Runtime.js.map +0 -1
- package/dist/RuntimeMutationPolicy.d.ts +0 -2
- package/dist/RuntimeMutationPolicy.d.ts.map +0 -1
- package/dist/RuntimeMutationPolicy.js +0 -2
- package/dist/RuntimeMutationPolicy.js.map +0 -1
- package/dist/Serializer.d.ts +0 -2
- package/dist/Serializer.d.ts.map +0 -1
- package/dist/Serializer.js +0 -2
- package/dist/Serializer.js.map +0 -1
- package/dist/Validation.d.ts +0 -2
- package/dist/Validation.d.ts.map +0 -1
- package/dist/Validation.js +0 -2
- package/dist/Validation.js.map +0 -1
- package/dist/ValidationPipeline.d.ts +0 -2
- package/dist/ValidationPipeline.d.ts.map +0 -1
- package/dist/ValidationPipeline.js +0 -2
- package/dist/ValidationPipeline.js.map +0 -1
- package/dist/discovery/Discovery.d.ts +0 -5
- package/dist/discovery/Discovery.d.ts.map +0 -1
- package/dist/discovery/Discovery.js +0 -63
- package/dist/discovery/Discovery.js.map +0 -1
- package/dist/errors/FieldErrorRenderer.d.ts +0 -17
- package/dist/errors/FieldErrorRenderer.d.ts.map +0 -1
- package/dist/errors/FieldErrorRenderer.js +0 -115
- package/dist/errors/FieldErrorRenderer.js.map +0 -1
- package/dist/field/FieldNode.d.ts +0 -38
- package/dist/field/FieldNode.d.ts.map +0 -1
- package/dist/field/FieldNode.js +0 -154
- package/dist/field/FieldNode.js.map +0 -1
- package/dist/field/FieldRegistry.d.ts +0 -19
- package/dist/field/FieldRegistry.d.ts.map +0 -1
- package/dist/field/FieldRegistry.js +0 -46
- package/dist/field/FieldRegistry.js.map +0 -1
- package/dist/field/FieldState.d.ts +0 -13
- package/dist/field/FieldState.d.ts.map +0 -1
- package/dist/field/FieldState.js +0 -2
- package/dist/field/FieldState.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/runtime/Runtime.d.ts +0 -137
- package/dist/runtime/Runtime.d.ts.map +0 -1
- package/dist/runtime/Runtime.js +0 -1374
- package/dist/runtime/Runtime.js.map +0 -1
- package/dist/runtime/RuntimeMutationPolicy.d.ts +0 -3
- package/dist/runtime/RuntimeMutationPolicy.d.ts.map +0 -1
- package/dist/runtime/RuntimeMutationPolicy.js +0 -34
- package/dist/runtime/RuntimeMutationPolicy.js.map +0 -1
- package/dist/serialization/Serializer.d.ts +0 -52
- package/dist/serialization/Serializer.d.ts.map +0 -1
- package/dist/serialization/Serializer.js +0 -115
- package/dist/serialization/Serializer.js.map +0 -1
- package/dist/shared/types.d.ts +0 -66
- package/dist/shared/types.d.ts.map +0 -1
- package/dist/shared/types.js +0 -2
- package/dist/shared/types.js.map +0 -1
- package/dist/tsdoc-metadata.json +0 -11
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validation/Validation.d.ts +0 -61
- package/dist/validation/Validation.d.ts.map +0 -1
- package/dist/validation/Validation.js +0 -34
- package/dist/validation/Validation.js.map +0 -1
- package/dist/validation/ValidationPipeline.d.ts +0 -61
- package/dist/validation/ValidationPipeline.d.ts.map +0 -1
- package/dist/validation/ValidationPipeline.js +0 -73
- package/dist/validation/ValidationPipeline.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FieldNode: () => FieldNode,
|
|
24
|
+
Runtime: () => Runtime,
|
|
25
|
+
Serializer: () => Serializer,
|
|
26
|
+
arrayItemPath: () => arrayItemPath,
|
|
27
|
+
createValidationPipeline: () => createValidationPipeline,
|
|
28
|
+
createValidator: () => createValidator,
|
|
29
|
+
formPath: () => formPath,
|
|
30
|
+
getByPath: () => getByPath,
|
|
31
|
+
isArrayItemPath: () => isArrayItemPath,
|
|
32
|
+
matchArrayItemPath: () => matchArrayItemPath,
|
|
33
|
+
normalizePath: () => normalizePath,
|
|
34
|
+
parseArrayItemPath: () => parseArrayItemPath,
|
|
35
|
+
remapArrayItemIndex: () => remapArrayItemIndex,
|
|
36
|
+
setByPath: () => setByPath,
|
|
37
|
+
shiftArrayItemIndex: () => shiftArrayItemIndex,
|
|
38
|
+
splitPath: () => splitPath,
|
|
39
|
+
toNestedValues: () => toNestedValues
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/Path.ts
|
|
44
|
+
var BLOCKED_PROPERTIES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
45
|
+
function formPath(...segments) {
|
|
46
|
+
return segments.map((segment) => String(segment).trim()).filter(Boolean).join(".");
|
|
47
|
+
}
|
|
48
|
+
function arrayItemPath(arrayName, index, ...segments) {
|
|
49
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
50
|
+
throw new Error("arrayItemPath index must be a non-negative integer.");
|
|
51
|
+
}
|
|
52
|
+
return formPath(arrayName, index, ...segments);
|
|
53
|
+
}
|
|
54
|
+
function splitPath(path) {
|
|
55
|
+
if (!path) return [];
|
|
56
|
+
return path.replace(/\[/g, ".").replace(/\]/g, "").split(".").map((segment) => segment.replace(/['"]/g, "").trim()).filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
function normalizePath(path) {
|
|
59
|
+
return splitPath(path).join(".");
|
|
60
|
+
}
|
|
61
|
+
function parseArrayItemPath(path) {
|
|
62
|
+
const parts = splitPath(path);
|
|
63
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
64
|
+
const next = parts[i + 1];
|
|
65
|
+
if (/^\d+$/.test(next)) {
|
|
66
|
+
const arrayName = parts.slice(0, i + 1).join(".");
|
|
67
|
+
const index = Number(next);
|
|
68
|
+
const subPath = parts.slice(i + 2).join(".");
|
|
69
|
+
return { arrayName, index, subPath };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function isArrayItemPath(path, arrayName) {
|
|
75
|
+
const parsed = parseArrayItemPath(path);
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (arrayName !== void 0 && parsed.arrayName !== arrayName) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
function matchArrayItemPath(path, arrayName) {
|
|
85
|
+
const parsed = parseArrayItemPath(path);
|
|
86
|
+
if (!parsed || parsed.arrayName !== arrayName) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return { index: parsed.index, subPath: parsed.subPath };
|
|
90
|
+
}
|
|
91
|
+
function shiftArrayItemIndex(path, arrayName, fromIndex, delta) {
|
|
92
|
+
const parsed = parseArrayItemPath(path);
|
|
93
|
+
if (!parsed || parsed.arrayName !== arrayName || parsed.index < fromIndex) {
|
|
94
|
+
return path;
|
|
95
|
+
}
|
|
96
|
+
const nextIndex = parsed.index + delta;
|
|
97
|
+
if (nextIndex < 0) {
|
|
98
|
+
throw new Error("shiftArrayItemIndex resulted in a negative index.");
|
|
99
|
+
}
|
|
100
|
+
return parsed.subPath ? arrayItemPath(arrayName, nextIndex, parsed.subPath) : arrayItemPath(arrayName, nextIndex);
|
|
101
|
+
}
|
|
102
|
+
function remapArrayItemIndex(path, arrayName, oldIndex, newIndex) {
|
|
103
|
+
if (!Number.isInteger(newIndex) || newIndex < 0) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"remapArrayItemIndex newIndex must be a non-negative integer."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const parsed = parseArrayItemPath(path);
|
|
109
|
+
if (!parsed || parsed.arrayName !== arrayName || parsed.index !== oldIndex) {
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
return parsed.subPath ? arrayItemPath(arrayName, newIndex, parsed.subPath) : arrayItemPath(arrayName, newIndex);
|
|
113
|
+
}
|
|
114
|
+
function getByPath(target, path, defaultValue) {
|
|
115
|
+
const parts = typeof path === "string" ? splitPath(path) : path;
|
|
116
|
+
if (parts.length === 0) {
|
|
117
|
+
return target !== void 0 ? target : defaultValue;
|
|
118
|
+
}
|
|
119
|
+
let current = target;
|
|
120
|
+
for (const part of parts) {
|
|
121
|
+
if (current === null || current === void 0) {
|
|
122
|
+
return defaultValue;
|
|
123
|
+
}
|
|
124
|
+
if (BLOCKED_PROPERTIES.has(part)) {
|
|
125
|
+
return defaultValue;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(current)) {
|
|
128
|
+
const index = Number(part);
|
|
129
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
130
|
+
return defaultValue;
|
|
131
|
+
}
|
|
132
|
+
current = current[index];
|
|
133
|
+
} else if (typeof current === "object") {
|
|
134
|
+
current = current[part];
|
|
135
|
+
} else {
|
|
136
|
+
return defaultValue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return current !== void 0 ? current : defaultValue;
|
|
140
|
+
}
|
|
141
|
+
function setByPath(target, path, value) {
|
|
142
|
+
const parts = typeof path === "string" ? splitPath(path) : path;
|
|
143
|
+
if (parts.length === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let current = target;
|
|
147
|
+
for (let i = 0; i < parts.length; i++) {
|
|
148
|
+
const part = parts[i];
|
|
149
|
+
if (BLOCKED_PROPERTIES.has(part)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const isLast = i === parts.length - 1;
|
|
153
|
+
const nextPart = parts[i + 1];
|
|
154
|
+
const nextIsIndex = nextPart !== void 0 && /^\d+$/.test(nextPart);
|
|
155
|
+
if (Array.isArray(current)) {
|
|
156
|
+
const index = Number(part);
|
|
157
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (isLast) {
|
|
161
|
+
current[index] = value;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (current[index] === void 0 || current[index] === null) {
|
|
165
|
+
current[index] = nextIsIndex ? [] : {};
|
|
166
|
+
}
|
|
167
|
+
current = current[index];
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (typeof current === "object" && current !== null) {
|
|
171
|
+
if (isLast) {
|
|
172
|
+
current[part] = value;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (current[part] === void 0 || current[part] === null) {
|
|
176
|
+
current[part] = nextIsIndex ? [] : {};
|
|
177
|
+
}
|
|
178
|
+
current = current[part];
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function toNestedValues(flatValues) {
|
|
185
|
+
const nested = {};
|
|
186
|
+
for (const [path, value] of Object.entries(flatValues)) {
|
|
187
|
+
const parts = splitPath(path);
|
|
188
|
+
if (parts.length === 0) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
setByPath(nested, parts, value);
|
|
192
|
+
}
|
|
193
|
+
return nested;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/serialization/Serializer.ts
|
|
197
|
+
var Serializer = class {
|
|
198
|
+
/**
|
|
199
|
+
* Native FormData serialization baseline (100% backward compatible with browser form submission).
|
|
200
|
+
*/
|
|
201
|
+
serialize(form) {
|
|
202
|
+
const formData = new FormData(form);
|
|
203
|
+
const values = {};
|
|
204
|
+
for (const key of new Set(formData.keys())) {
|
|
205
|
+
const entries = formData.getAll(key);
|
|
206
|
+
values[key] = entries.length === 1 ? entries[0] : entries;
|
|
207
|
+
}
|
|
208
|
+
return values;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Field-node-driven serialization supporting type preservation, unchecked checkbox coercion,
|
|
212
|
+
* disabled filtering, and custom element serializers.
|
|
213
|
+
*/
|
|
214
|
+
serializeFields(fields, options = {}) {
|
|
215
|
+
const {
|
|
216
|
+
includeDisabled = false,
|
|
217
|
+
coerceUncheckedCheckboxes = true,
|
|
218
|
+
valueMode = "typed",
|
|
219
|
+
customSerializers = {},
|
|
220
|
+
nesting = "flat"
|
|
221
|
+
} = options;
|
|
222
|
+
const values = {};
|
|
223
|
+
const groupedArrays = /* @__PURE__ */ new Map();
|
|
224
|
+
for (const node of fields) {
|
|
225
|
+
const element = node.element;
|
|
226
|
+
const name = node.name;
|
|
227
|
+
if (!name) continue;
|
|
228
|
+
if (!includeDisabled && "disabled" in element && element.disabled) continue;
|
|
229
|
+
let value;
|
|
230
|
+
const tagName = element.tagName.toLowerCase();
|
|
231
|
+
const inputType = element instanceof HTMLInputElement ? element.type : void 0;
|
|
232
|
+
const customExtractor = customSerializers[tagName] ?? (inputType ? customSerializers[inputType] : void 0);
|
|
233
|
+
if (customExtractor) {
|
|
234
|
+
value = customExtractor(element, node);
|
|
235
|
+
} else if (valueMode === "raw") {
|
|
236
|
+
if (element instanceof HTMLSelectElement && element.multiple) {
|
|
237
|
+
value = Array.from(element.selectedOptions).map((o) => o.value);
|
|
238
|
+
} else {
|
|
239
|
+
value = element.value;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
value = node.value;
|
|
243
|
+
}
|
|
244
|
+
if (!customExtractor && element instanceof HTMLInputElement && element.type === "radio") {
|
|
245
|
+
if (element.checked) {
|
|
246
|
+
values[name] = valueMode === "raw" ? element.value || "on" : node.value;
|
|
247
|
+
} else if (!(name in values)) {
|
|
248
|
+
values[name] = void 0;
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!customExtractor && element instanceof HTMLInputElement && element.type === "checkbox") {
|
|
253
|
+
const hasExplicitValue = element.hasAttribute("value") && element.getAttribute("value") !== "on";
|
|
254
|
+
if (hasExplicitValue) {
|
|
255
|
+
if (!element.checked) {
|
|
256
|
+
if (!(name in values)) {
|
|
257
|
+
values[name] = [];
|
|
258
|
+
} else if (!Array.isArray(values[name])) {
|
|
259
|
+
const arr = [values[name]];
|
|
260
|
+
groupedArrays.set(name, arr);
|
|
261
|
+
values[name] = arr;
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
value = element.value;
|
|
266
|
+
} else {
|
|
267
|
+
if (element.checked) {
|
|
268
|
+
value = valueMode === "raw" ? element.value || "on" : true;
|
|
269
|
+
} else {
|
|
270
|
+
if (coerceUncheckedCheckboxes) {
|
|
271
|
+
values[name] = false;
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (name in values) {
|
|
278
|
+
if (!groupedArrays.has(name)) {
|
|
279
|
+
const initial = values[name];
|
|
280
|
+
const arr = Array.isArray(initial) ? [...initial] : [initial];
|
|
281
|
+
groupedArrays.set(name, arr);
|
|
282
|
+
values[name] = arr;
|
|
283
|
+
}
|
|
284
|
+
groupedArrays.get(name).push(value);
|
|
285
|
+
} else {
|
|
286
|
+
values[name] = value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (nesting === "nested") {
|
|
290
|
+
return toNestedValues(values);
|
|
291
|
+
}
|
|
292
|
+
return values;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// src/field/FieldNode.ts
|
|
297
|
+
var FieldNode = class {
|
|
298
|
+
constructor(element) {
|
|
299
|
+
this.element = element;
|
|
300
|
+
}
|
|
301
|
+
element;
|
|
302
|
+
dirty = false;
|
|
303
|
+
touched = false;
|
|
304
|
+
mounted = true;
|
|
305
|
+
initialValue = void 0;
|
|
306
|
+
stateValue = void 0;
|
|
307
|
+
error;
|
|
308
|
+
validating = false;
|
|
309
|
+
get name() {
|
|
310
|
+
return this.element.name;
|
|
311
|
+
}
|
|
312
|
+
get tag() {
|
|
313
|
+
return this.element.tagName.toLowerCase();
|
|
314
|
+
}
|
|
315
|
+
get type() {
|
|
316
|
+
if (this.element instanceof HTMLInputElement) {
|
|
317
|
+
return this.element.type;
|
|
318
|
+
}
|
|
319
|
+
if (this.element instanceof HTMLSelectElement) {
|
|
320
|
+
return this.element.multiple ? "select-multiple" : "select";
|
|
321
|
+
}
|
|
322
|
+
return "textarea";
|
|
323
|
+
}
|
|
324
|
+
get value() {
|
|
325
|
+
if (this.element instanceof HTMLInputElement) {
|
|
326
|
+
switch (this.element.type) {
|
|
327
|
+
case "checkbox":
|
|
328
|
+
return this.element.checked;
|
|
329
|
+
case "radio":
|
|
330
|
+
return this.element.checked ? this.element.value : void 0;
|
|
331
|
+
case "number":
|
|
332
|
+
case "range":
|
|
333
|
+
return this.element.value === "" || Number.isNaN(this.element.valueAsNumber) ? void 0 : this.element.valueAsNumber;
|
|
334
|
+
case "file":
|
|
335
|
+
return this.element.files;
|
|
336
|
+
default:
|
|
337
|
+
return this.element.value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (this.element instanceof HTMLSelectElement) {
|
|
341
|
+
return this.element.multiple ? Array.from(this.element.selectedOptions).map((o) => o.value) : this.element.value;
|
|
342
|
+
}
|
|
343
|
+
return this.element.value;
|
|
344
|
+
}
|
|
345
|
+
focus() {
|
|
346
|
+
this.element.focus();
|
|
347
|
+
}
|
|
348
|
+
blur() {
|
|
349
|
+
this.element.blur();
|
|
350
|
+
}
|
|
351
|
+
get isDirty() {
|
|
352
|
+
return this.dirty;
|
|
353
|
+
}
|
|
354
|
+
get isTouched() {
|
|
355
|
+
return this.touched;
|
|
356
|
+
}
|
|
357
|
+
get isMounted() {
|
|
358
|
+
return this.mounted;
|
|
359
|
+
}
|
|
360
|
+
get isValidating() {
|
|
361
|
+
return this.validating;
|
|
362
|
+
}
|
|
363
|
+
setValidating(validating) {
|
|
364
|
+
this.validating = validating;
|
|
365
|
+
}
|
|
366
|
+
// --- New state API (replaces FieldStateStore) ---
|
|
367
|
+
register(initialValue) {
|
|
368
|
+
this.mounted = true;
|
|
369
|
+
this.initialValue = initialValue;
|
|
370
|
+
this.stateValue = initialValue;
|
|
371
|
+
this.dirty = false;
|
|
372
|
+
this.touched = false;
|
|
373
|
+
this.error = void 0;
|
|
374
|
+
this.validating = false;
|
|
375
|
+
}
|
|
376
|
+
unregister() {
|
|
377
|
+
this.mounted = false;
|
|
378
|
+
}
|
|
379
|
+
getState() {
|
|
380
|
+
return {
|
|
381
|
+
name: this.name,
|
|
382
|
+
dirty: this.dirty,
|
|
383
|
+
touched: this.touched,
|
|
384
|
+
mounted: this.mounted,
|
|
385
|
+
initialValue: this.initialValue,
|
|
386
|
+
value: this.stateValue,
|
|
387
|
+
error: this.error,
|
|
388
|
+
validating: this.validating
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
updateValue(value) {
|
|
392
|
+
const next = value === void 0 ? this.value : value;
|
|
393
|
+
this.stateValue = next;
|
|
394
|
+
this.dirty = !isEqualValue(this.initialValue, next);
|
|
395
|
+
}
|
|
396
|
+
markTouched() {
|
|
397
|
+
this.touched = true;
|
|
398
|
+
}
|
|
399
|
+
setError(message) {
|
|
400
|
+
this.error = message;
|
|
401
|
+
}
|
|
402
|
+
reset(value) {
|
|
403
|
+
this.initialValue = value;
|
|
404
|
+
this.stateValue = value;
|
|
405
|
+
this.dirty = false;
|
|
406
|
+
this.touched = false;
|
|
407
|
+
this.error = void 0;
|
|
408
|
+
this.validating = false;
|
|
409
|
+
}
|
|
410
|
+
// --- Lifecycle helpers ---
|
|
411
|
+
// Called when the browser emits input on the control
|
|
412
|
+
handleInput(value) {
|
|
413
|
+
this.updateValue(value);
|
|
414
|
+
}
|
|
415
|
+
// Called when the browser emits change on the control
|
|
416
|
+
handleChange(value) {
|
|
417
|
+
this.updateValue(value);
|
|
418
|
+
}
|
|
419
|
+
// Called when the control is blurred
|
|
420
|
+
handleBlur() {
|
|
421
|
+
this.markTouched();
|
|
422
|
+
}
|
|
423
|
+
// Called when the form is reset
|
|
424
|
+
handleReset(value) {
|
|
425
|
+
const initial = value ?? this.initialValue ?? this.value;
|
|
426
|
+
this.reset(initial);
|
|
427
|
+
this.setError(void 0);
|
|
428
|
+
}
|
|
429
|
+
// Called when the form is submitted (mark touched)
|
|
430
|
+
handleSubmit() {
|
|
431
|
+
this.markTouched();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
function isEqualValue(left, right) {
|
|
435
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
436
|
+
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (left.length !== right.length) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
return left.every((value, index) => value === right[index]);
|
|
443
|
+
}
|
|
444
|
+
return left === right;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/discovery/Discovery.ts
|
|
448
|
+
var Discovery = class {
|
|
449
|
+
discover(form, registry) {
|
|
450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
451
|
+
for (const control of Array.from(form.elements)) {
|
|
452
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLSelectElement || control instanceof HTMLTextAreaElement) {
|
|
453
|
+
if (!control.name) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const name = control.name;
|
|
457
|
+
seen.add(name);
|
|
458
|
+
if (registry.has(name)) {
|
|
459
|
+
const existing = registry.get(name);
|
|
460
|
+
if (existing.element === control) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (control instanceof HTMLInputElement && control.type === "radio" && existing.element instanceof HTMLInputElement && existing.element.type === "radio") {
|
|
464
|
+
if (control.checked && !existing.element.checked) {
|
|
465
|
+
registry.unregister(name);
|
|
466
|
+
registry.register(new FieldNode(control));
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (control instanceof HTMLInputElement && control.type === "checkbox" && existing.element instanceof HTMLInputElement && existing.element.type === "checkbox" && existing.element.form === form) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
registry.unregister(name);
|
|
474
|
+
registry.register(new FieldNode(control));
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
registry.register(new FieldNode(control));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const [key, field] of registry.entries()) {
|
|
481
|
+
if (!seen.has(key) || field.element.name !== key) {
|
|
482
|
+
registry.unregister(key);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/errors/FieldErrorRenderer.ts
|
|
489
|
+
var FieldErrorRenderer = class _FieldErrorRenderer {
|
|
490
|
+
static nextErrorId = 0;
|
|
491
|
+
errorIds = /* @__PURE__ */ new WeakMap();
|
|
492
|
+
errorElements = /* @__PURE__ */ new WeakMap();
|
|
493
|
+
applyErrors(fields, errors, renderDomErrors = true) {
|
|
494
|
+
for (const field of fields) {
|
|
495
|
+
this.applyFieldError(field, errors[field.name], renderDomErrors);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
normalizeFields(fields) {
|
|
499
|
+
for (const field of fields) {
|
|
500
|
+
this.normalizeField(field);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
applyFieldError(field, message, renderDomErrors = true) {
|
|
504
|
+
const element = field.element;
|
|
505
|
+
try {
|
|
506
|
+
element.setCustomValidity(message ?? "");
|
|
507
|
+
} catch (_e) {
|
|
508
|
+
}
|
|
509
|
+
if (!renderDomErrors) {
|
|
510
|
+
this.removeErrorElements(this.findExistingErrorElements(element));
|
|
511
|
+
this.errorElements.delete(element);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!message) {
|
|
515
|
+
this.removeErrorElements(this.findExistingErrorElements(element));
|
|
516
|
+
this.errorElements.delete(element);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
let errorElement = this.getPrimaryErrorElement(element);
|
|
520
|
+
if (!errorElement) {
|
|
521
|
+
errorElement = this.createErrorElement(field);
|
|
522
|
+
element.insertAdjacentElement("afterend", errorElement);
|
|
523
|
+
}
|
|
524
|
+
errorElement.textContent = message;
|
|
525
|
+
this.errorElements.set(element, errorElement);
|
|
526
|
+
}
|
|
527
|
+
normalizeField(field) {
|
|
528
|
+
const element = field.element;
|
|
529
|
+
const existing = this.findExistingErrorElements(element);
|
|
530
|
+
if (existing.length === 0) {
|
|
531
|
+
this.errorElements.delete(element);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const keep = existing[0];
|
|
535
|
+
this.removeErrorElements(existing.slice(1));
|
|
536
|
+
this.decorateErrorElement(field, keep);
|
|
537
|
+
this.errorElements.set(element, keep);
|
|
538
|
+
}
|
|
539
|
+
getPrimaryErrorElement(element) {
|
|
540
|
+
const stored = this.errorElements.get(element);
|
|
541
|
+
if (stored?.isConnected) {
|
|
542
|
+
const existing2 = this.findExistingErrorElements(element);
|
|
543
|
+
if (existing2.length > 1) {
|
|
544
|
+
this.removeErrorElements(existing2.slice(1));
|
|
545
|
+
}
|
|
546
|
+
return existing2[0] ?? stored;
|
|
547
|
+
}
|
|
548
|
+
const existing = this.findExistingErrorElements(element);
|
|
549
|
+
if (existing.length === 0) {
|
|
550
|
+
return void 0;
|
|
551
|
+
}
|
|
552
|
+
const keep = existing[0];
|
|
553
|
+
this.removeErrorElements(existing.slice(1));
|
|
554
|
+
this.errorElements.set(element, keep);
|
|
555
|
+
return keep;
|
|
556
|
+
}
|
|
557
|
+
findExistingErrorElements(element) {
|
|
558
|
+
const results = [];
|
|
559
|
+
let node = element.nextElementSibling;
|
|
560
|
+
while (node) {
|
|
561
|
+
if (node instanceof HTMLElement) {
|
|
562
|
+
const id = node.id ?? "";
|
|
563
|
+
const aria = node.getAttribute("aria-live");
|
|
564
|
+
const isRuntimeLabel = node.dataset.nativeFormError === "true" || id.startsWith("native-form-error-") || aria === "polite";
|
|
565
|
+
if (isRuntimeLabel) {
|
|
566
|
+
results.push(node);
|
|
567
|
+
node = node.nextElementSibling;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
return results;
|
|
574
|
+
}
|
|
575
|
+
removeErrorElements(elements) {
|
|
576
|
+
for (const element of elements) {
|
|
577
|
+
element.remove();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
createErrorElement(field) {
|
|
581
|
+
const error = document.createElement("div");
|
|
582
|
+
this.decorateErrorElement(field, error);
|
|
583
|
+
error.style.color = "red";
|
|
584
|
+
error.style.fontSize = "0.875rem";
|
|
585
|
+
error.style.marginTop = "0.25rem";
|
|
586
|
+
return error;
|
|
587
|
+
}
|
|
588
|
+
decorateErrorElement(field, element) {
|
|
589
|
+
element.id = this.getOrCreateErrorId(field.element);
|
|
590
|
+
element.dataset.nativeFormError = "true";
|
|
591
|
+
element.setAttribute("data-for", field.name);
|
|
592
|
+
element.setAttribute("aria-live", "polite");
|
|
593
|
+
}
|
|
594
|
+
getOrCreateErrorId(element) {
|
|
595
|
+
const existing = this.errorIds.get(element);
|
|
596
|
+
if (existing) {
|
|
597
|
+
return existing;
|
|
598
|
+
}
|
|
599
|
+
const created = `native-form-error-${_FieldErrorRenderer.nextErrorId++}`;
|
|
600
|
+
this.errorIds.set(element, created);
|
|
601
|
+
return created;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// src/field/FieldRegistry.ts
|
|
606
|
+
var FieldRegistry = class {
|
|
607
|
+
fields = /* @__PURE__ */ new Map();
|
|
608
|
+
// Optional change callback used by Runtime to observe mount/unmount events.
|
|
609
|
+
onChange;
|
|
610
|
+
register(field) {
|
|
611
|
+
this.fields.set(field.name, field);
|
|
612
|
+
this.onChange?.({ type: "mount", field });
|
|
613
|
+
}
|
|
614
|
+
unregister(name) {
|
|
615
|
+
const field = this.fields.get(name);
|
|
616
|
+
if (field) {
|
|
617
|
+
field.unregister();
|
|
618
|
+
this.onChange?.({ type: "unmount", field });
|
|
619
|
+
}
|
|
620
|
+
this.fields.delete(name);
|
|
621
|
+
}
|
|
622
|
+
get(name) {
|
|
623
|
+
return this.fields.get(name);
|
|
624
|
+
}
|
|
625
|
+
has(name) {
|
|
626
|
+
return this.fields.has(name);
|
|
627
|
+
}
|
|
628
|
+
values() {
|
|
629
|
+
return [...this.fields.values()];
|
|
630
|
+
}
|
|
631
|
+
keys() {
|
|
632
|
+
return Array.from(this.fields.keys());
|
|
633
|
+
}
|
|
634
|
+
entries() {
|
|
635
|
+
return Array.from(this.fields.entries());
|
|
636
|
+
}
|
|
637
|
+
clear() {
|
|
638
|
+
for (const field of this.fields.values()) {
|
|
639
|
+
field.unregister();
|
|
640
|
+
this.onChange?.({ type: "unmount", field });
|
|
641
|
+
}
|
|
642
|
+
this.fields.clear();
|
|
643
|
+
}
|
|
644
|
+
get size() {
|
|
645
|
+
return this.fields.size;
|
|
646
|
+
}
|
|
647
|
+
getAll() {
|
|
648
|
+
return Array.from(this.fields.values());
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// src/runtime/RuntimeMutationPolicy.ts
|
|
653
|
+
var runtimeMutationObserverOptions = {
|
|
654
|
+
childList: true,
|
|
655
|
+
subtree: true,
|
|
656
|
+
attributes: true,
|
|
657
|
+
attributeFilter: ["name", "disabled"]
|
|
658
|
+
};
|
|
659
|
+
function isRuntimeMutationRelevant(mutation) {
|
|
660
|
+
if (mutation.type === "attributes") {
|
|
661
|
+
return isNativeFormControl(mutation.target);
|
|
662
|
+
}
|
|
663
|
+
if (mutation.type === "childList") {
|
|
664
|
+
const nodes = [...mutation.addedNodes, ...mutation.removedNodes];
|
|
665
|
+
return nodes.some((node) => {
|
|
666
|
+
if (isErrorLabel(node)) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
return isNativeFormControl(node) || containsNativeFormControl(node);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
function isNativeFormControl(node) {
|
|
675
|
+
return node instanceof HTMLInputElement || node instanceof HTMLSelectElement || node instanceof HTMLTextAreaElement;
|
|
676
|
+
}
|
|
677
|
+
function containsNativeFormControl(node) {
|
|
678
|
+
return node instanceof HTMLElement && Boolean(node.querySelector("input[name], select[name], textarea[name]"));
|
|
679
|
+
}
|
|
680
|
+
function isErrorLabel(node) {
|
|
681
|
+
return node instanceof HTMLElement && node.dataset.nativeFormError === "true";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/validation/Validation.ts
|
|
685
|
+
function createValidator(schema) {
|
|
686
|
+
if ("~standard" in schema) {
|
|
687
|
+
return {
|
|
688
|
+
async validate(values) {
|
|
689
|
+
const result = await schema["~standard"].validate(values);
|
|
690
|
+
if (result.issues) {
|
|
691
|
+
return {
|
|
692
|
+
success: false,
|
|
693
|
+
issues: result.issues.map((issue) => ({
|
|
694
|
+
name: schemaIssuePathToName(issue.path),
|
|
695
|
+
message: issue.message
|
|
696
|
+
}))
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
success: true,
|
|
701
|
+
values: result.value
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
return schema;
|
|
707
|
+
}
|
|
708
|
+
function schemaIssuePathToName(path) {
|
|
709
|
+
if (!path || path.length === 0) {
|
|
710
|
+
return "";
|
|
711
|
+
}
|
|
712
|
+
return path.map(
|
|
713
|
+
(segment) => typeof segment === "object" && segment !== null ? segment.key : segment
|
|
714
|
+
).map(String).join(".");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/validation/ValidationPipeline.ts
|
|
718
|
+
function createValidationPipeline(options = {}) {
|
|
719
|
+
const schemaOrValidator = options.schema ?? options.validator;
|
|
720
|
+
const validator = schemaOrValidator ? createValidator(schemaOrValidator) : void 0;
|
|
721
|
+
return {
|
|
722
|
+
capabilities: {
|
|
723
|
+
supportsFieldScopedValidation: !validator
|
|
724
|
+
},
|
|
725
|
+
async validate(values, context) {
|
|
726
|
+
if (!validator) {
|
|
727
|
+
const nativeIssues = getNativeValidationIssues(context.fields);
|
|
728
|
+
if (nativeIssues.length > 0) {
|
|
729
|
+
return {
|
|
730
|
+
success: false,
|
|
731
|
+
issues: nativeIssues
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
success: true,
|
|
736
|
+
values
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
return validator.validate(values, context);
|
|
740
|
+
},
|
|
741
|
+
async validateField(field, values, context) {
|
|
742
|
+
if (!validator) {
|
|
743
|
+
const issue = getNativeValidationIssue(field);
|
|
744
|
+
if (issue) {
|
|
745
|
+
return {
|
|
746
|
+
success: false,
|
|
747
|
+
issues: [issue]
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
success: true,
|
|
752
|
+
values
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return validator.validate(values, context);
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function getNativeValidationIssue(field) {
|
|
760
|
+
const validationMessage = field.validationMessage;
|
|
761
|
+
if (!validationMessage) {
|
|
762
|
+
return void 0;
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
name: field.name,
|
|
766
|
+
message: validationMessage
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function getNativeValidationIssues(fields) {
|
|
770
|
+
const issues = [];
|
|
771
|
+
for (const field of fields) {
|
|
772
|
+
const validationMessage = field.validationMessage;
|
|
773
|
+
if (validationMessage) {
|
|
774
|
+
issues.push({
|
|
775
|
+
name: field.name,
|
|
776
|
+
message: validationMessage
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return issues;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/runtime/Runtime.ts
|
|
784
|
+
var Runtime = class {
|
|
785
|
+
constructor(options = {}) {
|
|
786
|
+
this.options = options;
|
|
787
|
+
this.pipeline = options.pipeline ?? createValidationPipeline({
|
|
788
|
+
schema: options.schema,
|
|
789
|
+
validator: options.validator
|
|
790
|
+
});
|
|
791
|
+
this.registry.onChange = (e) => {
|
|
792
|
+
if (e.type === "mount") {
|
|
793
|
+
this.emitFieldEvent({ type: "field:mount", field: e.field.name });
|
|
794
|
+
} else {
|
|
795
|
+
this.emitFieldEvent({ type: "field:unmount", field: e.field.name });
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
options;
|
|
800
|
+
registry = new FieldRegistry();
|
|
801
|
+
discovery = new Discovery();
|
|
802
|
+
serializer = new Serializer();
|
|
803
|
+
fieldErrorRenderer = new FieldErrorRenderer();
|
|
804
|
+
form;
|
|
805
|
+
listeners = /* @__PURE__ */ new Set();
|
|
806
|
+
// Field state is now managed on each FieldNode instance.
|
|
807
|
+
observer;
|
|
808
|
+
validationController;
|
|
809
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
810
|
+
fieldValidationControllers = /* @__PURE__ */ new Map();
|
|
811
|
+
submitting = false;
|
|
812
|
+
validating = false;
|
|
813
|
+
errors = {};
|
|
814
|
+
preservedValuesStore = /* @__PURE__ */ Object.create(null);
|
|
815
|
+
isSyncingFields = false;
|
|
816
|
+
pipeline;
|
|
817
|
+
mount(form) {
|
|
818
|
+
this.form = form;
|
|
819
|
+
this.syncFields();
|
|
820
|
+
this.fieldErrorRenderer.normalizeFields(this.registry.values());
|
|
821
|
+
const values = this.extractCurrentValues();
|
|
822
|
+
for (const field of this.registry.getAll()) {
|
|
823
|
+
field.register(values[field.name]);
|
|
824
|
+
}
|
|
825
|
+
form.addEventListener("submit", this.handleSubmit);
|
|
826
|
+
form.addEventListener("input", this.handleFieldUpdate);
|
|
827
|
+
form.addEventListener("change", this.handleFieldUpdate);
|
|
828
|
+
form.addEventListener("blur", this.handleFieldBlur, true);
|
|
829
|
+
form.addEventListener("reset", this.handleReset);
|
|
830
|
+
this.observer = new MutationObserver(this.handleMutation);
|
|
831
|
+
this.observer.observe(form, runtimeMutationObserverOptions);
|
|
832
|
+
this.emitFormEvent({
|
|
833
|
+
type: "form:mount"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
unmount() {
|
|
837
|
+
this.clearDebounceTimers();
|
|
838
|
+
this.abortAllValidations();
|
|
839
|
+
if (this.form) {
|
|
840
|
+
this.form.removeEventListener("submit", this.handleSubmit);
|
|
841
|
+
this.form.removeEventListener("input", this.handleFieldUpdate);
|
|
842
|
+
this.form.removeEventListener("change", this.handleFieldUpdate);
|
|
843
|
+
this.form.removeEventListener("blur", this.handleFieldBlur, true);
|
|
844
|
+
this.form.removeEventListener("reset", this.handleReset);
|
|
845
|
+
}
|
|
846
|
+
this.observer?.disconnect();
|
|
847
|
+
this.observer = void 0;
|
|
848
|
+
this.fieldErrorRenderer.applyErrors(this.registry.values(), {});
|
|
849
|
+
this.registry.clear();
|
|
850
|
+
this.form = void 0;
|
|
851
|
+
this.errors = {};
|
|
852
|
+
this.preservedValuesStore = /* @__PURE__ */ Object.create(null);
|
|
853
|
+
this.submitting = false;
|
|
854
|
+
this.validating = false;
|
|
855
|
+
this.emitFormEvent({
|
|
856
|
+
type: "form:unmount"
|
|
857
|
+
});
|
|
858
|
+
this.listeners.clear();
|
|
859
|
+
}
|
|
860
|
+
clearDebounceTimers() {
|
|
861
|
+
for (const timer of this.debounceTimers.values()) {
|
|
862
|
+
clearTimeout(timer);
|
|
863
|
+
}
|
|
864
|
+
this.debounceTimers.clear();
|
|
865
|
+
}
|
|
866
|
+
abortAllValidations() {
|
|
867
|
+
this.validationController?.abort();
|
|
868
|
+
this.validationController = void 0;
|
|
869
|
+
for (const controller of this.fieldValidationControllers.values()) {
|
|
870
|
+
controller.abort();
|
|
871
|
+
}
|
|
872
|
+
this.fieldValidationControllers.clear();
|
|
873
|
+
for (const field of this.registry.values()) {
|
|
874
|
+
field.setValidating(false);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
getAsyncValidator(name) {
|
|
878
|
+
if (!this.options.asyncValidators) {
|
|
879
|
+
return void 0;
|
|
880
|
+
}
|
|
881
|
+
if (this.options.asyncValidators[name]) {
|
|
882
|
+
return this.options.asyncValidators[name];
|
|
883
|
+
}
|
|
884
|
+
const normalized = normalizePath(name);
|
|
885
|
+
if (this.options.asyncValidators[normalized]) {
|
|
886
|
+
return this.options.asyncValidators[normalized];
|
|
887
|
+
}
|
|
888
|
+
for (const [key, validator] of Object.entries(this.options.asyncValidators)) {
|
|
889
|
+
if (normalizePath(key) === normalized) {
|
|
890
|
+
return validator;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return void 0;
|
|
894
|
+
}
|
|
895
|
+
serialize() {
|
|
896
|
+
if (!this.form) {
|
|
897
|
+
if (this.options.preserveUnmountedValues) {
|
|
898
|
+
return { ...this.preservedValuesStore };
|
|
899
|
+
}
|
|
900
|
+
throw new Error("Runtime is not mounted.");
|
|
901
|
+
}
|
|
902
|
+
if (this.options.preserveUnmountedValues) {
|
|
903
|
+
if (!this.isSyncingFields) {
|
|
904
|
+
this.syncFields();
|
|
905
|
+
}
|
|
906
|
+
const live = this.serializer.serialize(this.form);
|
|
907
|
+
return {
|
|
908
|
+
...this.preservedValuesStore,
|
|
909
|
+
...live
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return this.serializer.serialize(this.form);
|
|
913
|
+
}
|
|
914
|
+
serializeFields(options) {
|
|
915
|
+
if (this.options.preserveUnmountedValues && this.form && !this.isSyncingFields) {
|
|
916
|
+
this.syncFields();
|
|
917
|
+
}
|
|
918
|
+
const effectiveOptions = {
|
|
919
|
+
...this.options.serializeFieldsOptions,
|
|
920
|
+
...options
|
|
921
|
+
};
|
|
922
|
+
const nesting = effectiveOptions.nesting ?? this.options.nesting ?? "flat";
|
|
923
|
+
let nodes = this.registry.values();
|
|
924
|
+
if (this.form) {
|
|
925
|
+
const nodeList = [];
|
|
926
|
+
const nodeMap = new Map(this.registry.values().map((n) => [n.element, n]));
|
|
927
|
+
for (const control of Array.from(this.form.elements)) {
|
|
928
|
+
if ((control instanceof HTMLInputElement || control instanceof HTMLSelectElement || control instanceof HTMLTextAreaElement) && control.name) {
|
|
929
|
+
nodeList.push(nodeMap.get(control) ?? new FieldNode(control));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
nodes = nodeList;
|
|
933
|
+
}
|
|
934
|
+
if (this.options.preserveUnmountedValues) {
|
|
935
|
+
const live = this.serializer.serializeFields(nodes, {
|
|
936
|
+
...effectiveOptions,
|
|
937
|
+
nesting: "flat"
|
|
938
|
+
});
|
|
939
|
+
const merged = {
|
|
940
|
+
...this.preservedValuesStore,
|
|
941
|
+
...live
|
|
942
|
+
};
|
|
943
|
+
if (nesting === "nested") {
|
|
944
|
+
return toNestedValues(merged);
|
|
945
|
+
}
|
|
946
|
+
return merged;
|
|
947
|
+
}
|
|
948
|
+
return this.serializer.serializeFields(nodes, options);
|
|
949
|
+
}
|
|
950
|
+
get fields() {
|
|
951
|
+
return this.getFieldStates();
|
|
952
|
+
}
|
|
953
|
+
get serializationMode() {
|
|
954
|
+
return this.options.serializationMode ?? "native";
|
|
955
|
+
}
|
|
956
|
+
get preservedValues() {
|
|
957
|
+
if (this.options.preserveUnmountedValues && this.form && !this.isSyncingFields) {
|
|
958
|
+
this.syncFields();
|
|
959
|
+
}
|
|
960
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), this.preservedValuesStore);
|
|
961
|
+
}
|
|
962
|
+
extractCurrentValues() {
|
|
963
|
+
if (!this.form) {
|
|
964
|
+
if (this.options.preserveUnmountedValues) {
|
|
965
|
+
let res = { ...this.preservedValuesStore };
|
|
966
|
+
if (this.options.nesting === "nested") {
|
|
967
|
+
res = toNestedValues(res);
|
|
968
|
+
}
|
|
969
|
+
return res;
|
|
970
|
+
}
|
|
971
|
+
return {};
|
|
972
|
+
}
|
|
973
|
+
const serializeOpts = {
|
|
974
|
+
...this.options.serializeFieldsOptions,
|
|
975
|
+
...this.options.nesting ? { nesting: this.options.nesting } : {}
|
|
976
|
+
};
|
|
977
|
+
let values;
|
|
978
|
+
if (this.serializationMode === "typed") {
|
|
979
|
+
values = this.serializeFields(serializeOpts);
|
|
980
|
+
} else {
|
|
981
|
+
values = this.serialize();
|
|
982
|
+
if (this.options.nesting === "nested") {
|
|
983
|
+
values = toNestedValues(values);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return values;
|
|
987
|
+
}
|
|
988
|
+
subscribe(listener) {
|
|
989
|
+
this.listeners.add(listener);
|
|
990
|
+
return () => {
|
|
991
|
+
this.unsubscribe(listener);
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
unsubscribe(listener) {
|
|
995
|
+
this.listeners.delete(listener);
|
|
996
|
+
}
|
|
997
|
+
notify(event) {
|
|
998
|
+
for (const listener of this.listeners) {
|
|
999
|
+
listener(event);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
emitFieldEvent(event) {
|
|
1003
|
+
this.notify(event);
|
|
1004
|
+
}
|
|
1005
|
+
emitValidationEvent(event) {
|
|
1006
|
+
this.notify(event);
|
|
1007
|
+
}
|
|
1008
|
+
emitFormEvent(event) {
|
|
1009
|
+
this.notify(event);
|
|
1010
|
+
}
|
|
1011
|
+
submit(event) {
|
|
1012
|
+
return this.processSubmitFlow(event);
|
|
1013
|
+
}
|
|
1014
|
+
async validate() {
|
|
1015
|
+
const validation = await this.runValidation(this.extractCurrentValues());
|
|
1016
|
+
return validation.result;
|
|
1017
|
+
}
|
|
1018
|
+
async validateFields(fieldNames) {
|
|
1019
|
+
if (!this.form) {
|
|
1020
|
+
throw new Error("Runtime is not mounted.");
|
|
1021
|
+
}
|
|
1022
|
+
this.clearDebounceTimers();
|
|
1023
|
+
this.abortAllValidations();
|
|
1024
|
+
for (const name of fieldNames) {
|
|
1025
|
+
delete this.errors[name];
|
|
1026
|
+
const normalized = normalizePath(name);
|
|
1027
|
+
if (this.errors[normalized]) {
|
|
1028
|
+
delete this.errors[normalized];
|
|
1029
|
+
}
|
|
1030
|
+
const node = this.registry.get(name);
|
|
1031
|
+
node?.setError(void 0);
|
|
1032
|
+
}
|
|
1033
|
+
this.applyNativeErrors();
|
|
1034
|
+
const values = this.extractCurrentValues();
|
|
1035
|
+
const normalizedTargetNames = new Set(fieldNames.map(normalizePath));
|
|
1036
|
+
const controller = new AbortController();
|
|
1037
|
+
this.validationController = controller;
|
|
1038
|
+
this.validating = true;
|
|
1039
|
+
this.emitValidationEvent({
|
|
1040
|
+
type: "validation:start",
|
|
1041
|
+
values
|
|
1042
|
+
});
|
|
1043
|
+
const context = {
|
|
1044
|
+
form: this.form,
|
|
1045
|
+
signal: controller.signal,
|
|
1046
|
+
fields: this.getValidationFields()
|
|
1047
|
+
};
|
|
1048
|
+
const result = await this.pipeline.validate(values, context);
|
|
1049
|
+
if (this.validationController === controller) {
|
|
1050
|
+
this.validating = false;
|
|
1051
|
+
}
|
|
1052
|
+
if (controller.signal.aborted) {
|
|
1053
|
+
return {
|
|
1054
|
+
success: false,
|
|
1055
|
+
issues: []
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
const filteredIssues = [];
|
|
1059
|
+
if (!result.success && result.issues) {
|
|
1060
|
+
for (const issue of result.issues) {
|
|
1061
|
+
if (normalizedTargetNames.has(normalizePath(issue.name))) {
|
|
1062
|
+
filteredIssues.push(issue);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
for (const name of fieldNames) {
|
|
1067
|
+
const issue = filteredIssues.find(
|
|
1068
|
+
(iss) => normalizePath(iss.name) === normalizePath(name)
|
|
1069
|
+
);
|
|
1070
|
+
if (issue) {
|
|
1071
|
+
this.errors[name] = issue.message;
|
|
1072
|
+
const node = this.registry.get(name);
|
|
1073
|
+
node?.setError(issue.message);
|
|
1074
|
+
} else {
|
|
1075
|
+
delete this.errors[name];
|
|
1076
|
+
const normalized = normalizePath(name);
|
|
1077
|
+
if (this.errors[normalized]) {
|
|
1078
|
+
delete this.errors[normalized];
|
|
1079
|
+
}
|
|
1080
|
+
const node = this.registry.get(name);
|
|
1081
|
+
node?.setError(void 0);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
this.applyNativeErrors();
|
|
1085
|
+
if (this.options.asyncValidators) {
|
|
1086
|
+
const asyncPromises = [];
|
|
1087
|
+
const flat = this.serializationMode === "typed" ? this.serialize() : values;
|
|
1088
|
+
for (const name of fieldNames) {
|
|
1089
|
+
const issue = filteredIssues.find(
|
|
1090
|
+
(iss) => normalizePath(iss.name) === normalizePath(name)
|
|
1091
|
+
);
|
|
1092
|
+
if (issue) continue;
|
|
1093
|
+
const asyncValidator = this.getAsyncValidator(name);
|
|
1094
|
+
if (!asyncValidator) continue;
|
|
1095
|
+
const node = this.registry.get(name);
|
|
1096
|
+
node?.setValidating(true);
|
|
1097
|
+
const rawValue = values[name] ?? flat[name];
|
|
1098
|
+
asyncPromises.push(
|
|
1099
|
+
(async () => {
|
|
1100
|
+
try {
|
|
1101
|
+
const err = await asyncValidator(rawValue, {
|
|
1102
|
+
form: this.form,
|
|
1103
|
+
signal: controller.signal
|
|
1104
|
+
});
|
|
1105
|
+
if (err) {
|
|
1106
|
+
filteredIssues.push({ name, message: err });
|
|
1107
|
+
this.errors[name] = err;
|
|
1108
|
+
node?.setError(err);
|
|
1109
|
+
}
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1112
|
+
filteredIssues.push({ name, message: msg });
|
|
1113
|
+
this.errors[name] = msg;
|
|
1114
|
+
node?.setError(msg);
|
|
1115
|
+
} finally {
|
|
1116
|
+
node?.setValidating(false);
|
|
1117
|
+
}
|
|
1118
|
+
})()
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (asyncPromises.length > 0) {
|
|
1122
|
+
await Promise.all(asyncPromises);
|
|
1123
|
+
this.applyNativeErrors();
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
this.emitValidationEvent({
|
|
1127
|
+
type: "validation:end",
|
|
1128
|
+
errors: { ...this.errors },
|
|
1129
|
+
values
|
|
1130
|
+
});
|
|
1131
|
+
if (filteredIssues.length > 0) {
|
|
1132
|
+
return {
|
|
1133
|
+
success: false,
|
|
1134
|
+
issues: filteredIssues
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
success: true,
|
|
1139
|
+
values
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
handleSubmit = (event) => {
|
|
1143
|
+
event.preventDefault();
|
|
1144
|
+
void this.processSubmitFlow(event);
|
|
1145
|
+
};
|
|
1146
|
+
async processSubmitFlow(event) {
|
|
1147
|
+
if (!this.form) {
|
|
1148
|
+
throw new Error("Runtime is not mounted.");
|
|
1149
|
+
}
|
|
1150
|
+
this.clearDebounceTimers();
|
|
1151
|
+
this.abortAllValidations();
|
|
1152
|
+
const values = this.extractCurrentValues();
|
|
1153
|
+
for (const field of this.registry.values()) {
|
|
1154
|
+
field.handleSubmit();
|
|
1155
|
+
this.emitFieldEvent({
|
|
1156
|
+
type: "field:submit",
|
|
1157
|
+
field: field.name
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
const validation = await this.runValidation(values);
|
|
1161
|
+
const isStaleValidation = this.validationController !== void 0 && validation.signal !== this.validationController.signal;
|
|
1162
|
+
if (validation.aborted || isStaleValidation) {
|
|
1163
|
+
return validation.result;
|
|
1164
|
+
}
|
|
1165
|
+
if (!validation.result.success) {
|
|
1166
|
+
this.options.onInvalid?.(this.errors, values, event);
|
|
1167
|
+
this.emitFormEvent({
|
|
1168
|
+
type: "form:invalid",
|
|
1169
|
+
errors: this.errors,
|
|
1170
|
+
values
|
|
1171
|
+
});
|
|
1172
|
+
return validation.result;
|
|
1173
|
+
}
|
|
1174
|
+
if (this.options.asyncValidators && Object.keys(this.options.asyncValidators).length > 0) {
|
|
1175
|
+
const asyncIssues = [];
|
|
1176
|
+
const flat = this.serializationMode === "typed" ? this.serialize() : values;
|
|
1177
|
+
const promises = [];
|
|
1178
|
+
for (const [name, validator] of Object.entries(
|
|
1179
|
+
this.options.asyncValidators
|
|
1180
|
+
)) {
|
|
1181
|
+
const rawValue = values[name] ?? flat[name];
|
|
1182
|
+
const fieldNode = this.registry.get(name);
|
|
1183
|
+
if (fieldNode) {
|
|
1184
|
+
fieldNode.setValidating(true);
|
|
1185
|
+
}
|
|
1186
|
+
promises.push(
|
|
1187
|
+
(async () => {
|
|
1188
|
+
try {
|
|
1189
|
+
const err = await validator(rawValue, {
|
|
1190
|
+
form: this.form,
|
|
1191
|
+
signal: validation.signal
|
|
1192
|
+
});
|
|
1193
|
+
if (err) {
|
|
1194
|
+
asyncIssues.push({ name, message: err });
|
|
1195
|
+
}
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1198
|
+
asyncIssues.push({ name, message: msg });
|
|
1199
|
+
} finally {
|
|
1200
|
+
fieldNode?.setValidating(false);
|
|
1201
|
+
}
|
|
1202
|
+
})()
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
await Promise.all(promises);
|
|
1206
|
+
if (asyncIssues.length > 0) {
|
|
1207
|
+
for (const issue of asyncIssues) {
|
|
1208
|
+
this.errors[issue.name] = issue.message;
|
|
1209
|
+
const node = this.registry.get(issue.name);
|
|
1210
|
+
node?.setError(issue.message);
|
|
1211
|
+
}
|
|
1212
|
+
this.applyNativeErrors();
|
|
1213
|
+
this.options.onInvalid?.(this.errors, values, event);
|
|
1214
|
+
this.emitFormEvent({
|
|
1215
|
+
type: "form:invalid",
|
|
1216
|
+
errors: this.errors,
|
|
1217
|
+
values
|
|
1218
|
+
});
|
|
1219
|
+
return {
|
|
1220
|
+
success: false,
|
|
1221
|
+
issues: asyncIssues
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
this.submitting = true;
|
|
1226
|
+
this.emitFormEvent({
|
|
1227
|
+
type: "form:submit",
|
|
1228
|
+
values: validation.result.values
|
|
1229
|
+
});
|
|
1230
|
+
try {
|
|
1231
|
+
await this.options.onSubmit?.(validation.result.values, event);
|
|
1232
|
+
} finally {
|
|
1233
|
+
this.submitting = false;
|
|
1234
|
+
}
|
|
1235
|
+
return validation.result;
|
|
1236
|
+
}
|
|
1237
|
+
async runValidation(values) {
|
|
1238
|
+
if (!this.form) {
|
|
1239
|
+
throw new Error("Runtime is not mounted.");
|
|
1240
|
+
}
|
|
1241
|
+
this.validationController?.abort();
|
|
1242
|
+
const controller = new AbortController();
|
|
1243
|
+
this.validationController = controller;
|
|
1244
|
+
this.validating = true;
|
|
1245
|
+
this.emitValidationEvent({
|
|
1246
|
+
type: "validation:start",
|
|
1247
|
+
values
|
|
1248
|
+
});
|
|
1249
|
+
this.errors = {};
|
|
1250
|
+
for (const field of this.registry.values()) {
|
|
1251
|
+
field.setError(this.errors[field.name]);
|
|
1252
|
+
}
|
|
1253
|
+
this.applyNativeErrors();
|
|
1254
|
+
const result = await this.pipeline.validate(values, {
|
|
1255
|
+
form: this.form,
|
|
1256
|
+
signal: controller.signal,
|
|
1257
|
+
fields: this.getValidationFields()
|
|
1258
|
+
});
|
|
1259
|
+
if (!controller.signal.aborted) {
|
|
1260
|
+
this.applyValidationResult(result, values);
|
|
1261
|
+
if (this.validationController === controller) {
|
|
1262
|
+
this.validating = false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
result,
|
|
1267
|
+
signal: controller.signal,
|
|
1268
|
+
aborted: controller.signal.aborted
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
handleFieldUpdate = (event) => {
|
|
1272
|
+
const element = toNativeFormElement(event.target);
|
|
1273
|
+
if (!element?.name || !this.form) return;
|
|
1274
|
+
if (event.type === "input") {
|
|
1275
|
+
this.processInputFlow(element);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (event.type === "change") {
|
|
1279
|
+
this.processChangeFlow(element);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
this.processUpdateFlow(element);
|
|
1283
|
+
};
|
|
1284
|
+
processInputFlow(element) {
|
|
1285
|
+
const snapshot = this.prepareFieldFlow(element);
|
|
1286
|
+
if (!snapshot) {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const { node, values, value } = snapshot;
|
|
1290
|
+
node.handleInput(value);
|
|
1291
|
+
const mode = this.options.mode ?? "onChange";
|
|
1292
|
+
const reValidateMode = this.options.reValidateMode ?? "onChange";
|
|
1293
|
+
const hasExistingError = Boolean(
|
|
1294
|
+
resolveFieldError(this.errors, element.name)
|
|
1295
|
+
);
|
|
1296
|
+
const shouldValidate = mode === "onChange" || mode === "all" || hasExistingError && reValidateMode === "onChange" || mode === "onTouched" && node.isTouched;
|
|
1297
|
+
if (shouldValidate) {
|
|
1298
|
+
this.triggerFieldValidation(node, values);
|
|
1299
|
+
}
|
|
1300
|
+
this.emitFieldEvent({
|
|
1301
|
+
type: "field:input",
|
|
1302
|
+
field: element.name,
|
|
1303
|
+
value,
|
|
1304
|
+
values
|
|
1305
|
+
});
|
|
1306
|
+
this.emitFieldEvent({
|
|
1307
|
+
type: "field:update",
|
|
1308
|
+
field: element.name,
|
|
1309
|
+
value,
|
|
1310
|
+
values
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
processChangeFlow(element) {
|
|
1314
|
+
const snapshot = this.prepareFieldFlow(element);
|
|
1315
|
+
if (!snapshot) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const { node, values, value } = snapshot;
|
|
1319
|
+
node.handleChange(value);
|
|
1320
|
+
const mode = this.options.mode ?? "onChange";
|
|
1321
|
+
const reValidateMode = this.options.reValidateMode ?? "onChange";
|
|
1322
|
+
const hasExistingError = Boolean(
|
|
1323
|
+
resolveFieldError(this.errors, element.name)
|
|
1324
|
+
);
|
|
1325
|
+
const isDiscreteControl = element instanceof HTMLSelectElement || element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio");
|
|
1326
|
+
const shouldValidate = mode === "onChange" || mode === "all" || isDiscreteControl || hasExistingError && reValidateMode === "onChange" || mode === "onTouched" && node.isTouched;
|
|
1327
|
+
if (shouldValidate) {
|
|
1328
|
+
this.triggerFieldValidation(node, values);
|
|
1329
|
+
}
|
|
1330
|
+
this.emitFieldEvent({
|
|
1331
|
+
type: "field:change",
|
|
1332
|
+
field: element.name,
|
|
1333
|
+
value,
|
|
1334
|
+
values
|
|
1335
|
+
});
|
|
1336
|
+
this.emitFieldEvent({
|
|
1337
|
+
type: "field:update",
|
|
1338
|
+
field: element.name,
|
|
1339
|
+
value,
|
|
1340
|
+
values
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
processUpdateFlow(element) {
|
|
1344
|
+
const snapshot = this.prepareFieldFlow(element);
|
|
1345
|
+
if (!snapshot) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const { node, values, value } = snapshot;
|
|
1349
|
+
node.updateValue(value);
|
|
1350
|
+
if (this.options.preserveUnmountedValues) {
|
|
1351
|
+
if (element.name !== "__proto__" && element.name !== "constructor" && element.name !== "prototype") {
|
|
1352
|
+
if (value !== void 0) {
|
|
1353
|
+
this.preservedValuesStore[element.name] = value;
|
|
1354
|
+
} else {
|
|
1355
|
+
delete this.preservedValuesStore[element.name];
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const mode = this.options.mode ?? "onChange";
|
|
1360
|
+
const shouldValidate = mode === "onChange" || mode === "all" || mode === "onTouched" && node.isTouched;
|
|
1361
|
+
if (shouldValidate) {
|
|
1362
|
+
this.triggerFieldValidation(node, values);
|
|
1363
|
+
}
|
|
1364
|
+
this.emitFieldEvent({
|
|
1365
|
+
type: "field:update",
|
|
1366
|
+
field: element.name,
|
|
1367
|
+
value,
|
|
1368
|
+
values
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
prepareFieldFlow(element) {
|
|
1372
|
+
this.ensureField(element);
|
|
1373
|
+
const values = this.extractCurrentValues();
|
|
1374
|
+
const value = values[element.name];
|
|
1375
|
+
const node = this.registry.get(element.name);
|
|
1376
|
+
if (!node) {
|
|
1377
|
+
return void 0;
|
|
1378
|
+
}
|
|
1379
|
+
return { node, values, value };
|
|
1380
|
+
}
|
|
1381
|
+
triggerFieldValidation(node, values, immediate = false) {
|
|
1382
|
+
const debounceMs = this.options.debounceMs ?? 0;
|
|
1383
|
+
if (!immediate && debounceMs > 0) {
|
|
1384
|
+
const existing = this.debounceTimers.get(node.name);
|
|
1385
|
+
if (existing) {
|
|
1386
|
+
clearTimeout(existing);
|
|
1387
|
+
}
|
|
1388
|
+
const timer = setTimeout(() => {
|
|
1389
|
+
this.debounceTimers.delete(node.name);
|
|
1390
|
+
void this.executeFieldValidation(node, values);
|
|
1391
|
+
}, debounceMs);
|
|
1392
|
+
this.debounceTimers.set(node.name, timer);
|
|
1393
|
+
} else {
|
|
1394
|
+
const existing = this.debounceTimers.get(node.name);
|
|
1395
|
+
if (existing) {
|
|
1396
|
+
clearTimeout(existing);
|
|
1397
|
+
this.debounceTimers.delete(node.name);
|
|
1398
|
+
}
|
|
1399
|
+
void this.executeFieldValidation(node, values);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async executeFieldValidation(node, values) {
|
|
1403
|
+
const fieldName = node.name;
|
|
1404
|
+
const previousController = this.fieldValidationControllers.get(fieldName);
|
|
1405
|
+
if (previousController) {
|
|
1406
|
+
previousController.abort();
|
|
1407
|
+
this.fieldValidationControllers.delete(fieldName);
|
|
1408
|
+
}
|
|
1409
|
+
const fieldController = new AbortController();
|
|
1410
|
+
this.fieldValidationControllers.set(fieldName, fieldController);
|
|
1411
|
+
try {
|
|
1412
|
+
const validation = await this.runFieldValidation(node, values);
|
|
1413
|
+
const isStaleValidation = this.validationController !== void 0 && validation.signal !== this.validationController.signal;
|
|
1414
|
+
if (validation.aborted || fieldController.signal.aborted || isStaleValidation) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (!validation.result.success) {
|
|
1418
|
+
this.options.onInvalid?.(this.errors, values);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const asyncValidator = this.getAsyncValidator(fieldName);
|
|
1422
|
+
if (asyncValidator && !fieldController.signal.aborted) {
|
|
1423
|
+
node.setValidating(true);
|
|
1424
|
+
this.emitFieldEvent({
|
|
1425
|
+
type: "field:update",
|
|
1426
|
+
field: fieldName,
|
|
1427
|
+
value: values[fieldName],
|
|
1428
|
+
values
|
|
1429
|
+
});
|
|
1430
|
+
try {
|
|
1431
|
+
const context = {
|
|
1432
|
+
form: this.form,
|
|
1433
|
+
signal: fieldController.signal
|
|
1434
|
+
};
|
|
1435
|
+
const rawValue = values[fieldName];
|
|
1436
|
+
const asyncError = await asyncValidator(rawValue, context);
|
|
1437
|
+
if (!fieldController.signal.aborted) {
|
|
1438
|
+
if (asyncError) {
|
|
1439
|
+
this.errors[fieldName] = asyncError;
|
|
1440
|
+
node.setError(asyncError);
|
|
1441
|
+
this.options.onInvalid?.(this.errors, values);
|
|
1442
|
+
} else if (this.errors[fieldName]) {
|
|
1443
|
+
delete this.errors[fieldName];
|
|
1444
|
+
node.setError(void 0);
|
|
1445
|
+
}
|
|
1446
|
+
this.applyNativeErrors();
|
|
1447
|
+
}
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
if (!fieldController.signal.aborted) {
|
|
1450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1451
|
+
this.errors[fieldName] = message;
|
|
1452
|
+
node.setError(message);
|
|
1453
|
+
this.applyNativeErrors();
|
|
1454
|
+
this.options.onInvalid?.(this.errors, values);
|
|
1455
|
+
}
|
|
1456
|
+
} finally {
|
|
1457
|
+
if (!fieldController.signal.aborted) {
|
|
1458
|
+
node.setValidating(false);
|
|
1459
|
+
this.emitValidationEvent({
|
|
1460
|
+
type: "validation:end",
|
|
1461
|
+
errors: { ...this.errors },
|
|
1462
|
+
values
|
|
1463
|
+
});
|
|
1464
|
+
this.emitFieldEvent({
|
|
1465
|
+
type: "field:update",
|
|
1466
|
+
field: fieldName,
|
|
1467
|
+
value: values[fieldName],
|
|
1468
|
+
values
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
} finally {
|
|
1474
|
+
if (this.fieldValidationControllers.get(fieldName) === fieldController) {
|
|
1475
|
+
this.fieldValidationControllers.delete(fieldName);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
handleFieldBlur = (event) => {
|
|
1480
|
+
const element = toNativeFormElement(event.target);
|
|
1481
|
+
if (!element?.name || !this.form) return;
|
|
1482
|
+
this.ensureField(element);
|
|
1483
|
+
const values = this.extractCurrentValues();
|
|
1484
|
+
const value = values[element.name];
|
|
1485
|
+
const node = this.registry.get(element.name);
|
|
1486
|
+
node?.handleBlur();
|
|
1487
|
+
const mode = this.options.mode ?? "onChange";
|
|
1488
|
+
const reValidateMode = this.options.reValidateMode ?? "onChange";
|
|
1489
|
+
const hasExistingError = Boolean(
|
|
1490
|
+
resolveFieldError(this.errors, element.name)
|
|
1491
|
+
);
|
|
1492
|
+
const hasPendingDebounce = this.debounceTimers.has(element.name);
|
|
1493
|
+
const shouldValidate = mode === "onBlur" || mode === "onTouched" || mode === "all" || hasExistingError && reValidateMode === "onBlur" || hasPendingDebounce;
|
|
1494
|
+
if (shouldValidate && node) {
|
|
1495
|
+
this.triggerFieldValidation(node, values, true);
|
|
1496
|
+
}
|
|
1497
|
+
this.emitFieldEvent({
|
|
1498
|
+
type: "field:blur",
|
|
1499
|
+
field: element.name,
|
|
1500
|
+
value,
|
|
1501
|
+
values
|
|
1502
|
+
});
|
|
1503
|
+
};
|
|
1504
|
+
async runFieldValidation(field, values) {
|
|
1505
|
+
if (!this.form) {
|
|
1506
|
+
throw new Error("Runtime is not mounted.");
|
|
1507
|
+
}
|
|
1508
|
+
this.validationController?.abort();
|
|
1509
|
+
const controller = new AbortController();
|
|
1510
|
+
this.validationController = controller;
|
|
1511
|
+
this.validating = true;
|
|
1512
|
+
this.emitValidationEvent({
|
|
1513
|
+
type: "validation:start",
|
|
1514
|
+
values
|
|
1515
|
+
});
|
|
1516
|
+
const context = {
|
|
1517
|
+
form: this.form,
|
|
1518
|
+
signal: controller.signal,
|
|
1519
|
+
fields: this.getValidationFields()
|
|
1520
|
+
};
|
|
1521
|
+
const activeFieldMeta = this.toValidationField(field);
|
|
1522
|
+
const supportsFieldScopedValidation = this.pipeline.capabilities?.supportsFieldScopedValidation ?? true;
|
|
1523
|
+
const result = supportsFieldScopedValidation ? await this.pipeline.validateField(activeFieldMeta, values, context) : await this.pipeline.validate(values, context);
|
|
1524
|
+
if (!controller.signal.aborted) {
|
|
1525
|
+
this.applyFieldValidationResult(field, result, values);
|
|
1526
|
+
if (this.validationController === controller) {
|
|
1527
|
+
this.validating = false;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
result,
|
|
1532
|
+
signal: controller.signal,
|
|
1533
|
+
aborted: controller.signal.aborted
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
handleReset = () => {
|
|
1537
|
+
this.clearDebounceTimers();
|
|
1538
|
+
this.abortAllValidations();
|
|
1539
|
+
queueMicrotask(() => {
|
|
1540
|
+
if (!this.form) return;
|
|
1541
|
+
const values = this.extractCurrentValues();
|
|
1542
|
+
this.processResetFlow(values);
|
|
1543
|
+
});
|
|
1544
|
+
};
|
|
1545
|
+
processResetFlow(values) {
|
|
1546
|
+
for (const field of this.registry.values()) {
|
|
1547
|
+
field.handleReset(values[field.name]);
|
|
1548
|
+
this.emitFieldEvent({
|
|
1549
|
+
type: "field:reset",
|
|
1550
|
+
field: field.name,
|
|
1551
|
+
value: values[field.name],
|
|
1552
|
+
values
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
this.errors = {};
|
|
1556
|
+
this.preservedValuesStore = /* @__PURE__ */ Object.create(null);
|
|
1557
|
+
this.applyNativeErrors();
|
|
1558
|
+
this.emitFormEvent({
|
|
1559
|
+
type: "form:reset",
|
|
1560
|
+
values
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
getFieldState(name) {
|
|
1564
|
+
let node = this.registry.get(name);
|
|
1565
|
+
if (!node) {
|
|
1566
|
+
const normalized = normalizePath(name);
|
|
1567
|
+
for (const field of this.registry.values()) {
|
|
1568
|
+
if (normalizePath(field.name) === normalized) {
|
|
1569
|
+
node = field;
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return node?.getState();
|
|
1575
|
+
}
|
|
1576
|
+
getFieldStates() {
|
|
1577
|
+
return this.registry.values().map((f) => f.getState());
|
|
1578
|
+
}
|
|
1579
|
+
setOptions(options) {
|
|
1580
|
+
Object.assign(this.options, options);
|
|
1581
|
+
if (options.schema !== void 0 || options.validator !== void 0 || options.pipeline !== void 0) {
|
|
1582
|
+
this.pipeline = options.pipeline ?? createValidationPipeline({
|
|
1583
|
+
schema: options.schema,
|
|
1584
|
+
validator: options.validator
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
removeArrayItem(arrayName, index) {
|
|
1589
|
+
const nextErrors = {};
|
|
1590
|
+
let changed = false;
|
|
1591
|
+
for (const [path, error] of Object.entries(this.errors)) {
|
|
1592
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1593
|
+
if (!match) {
|
|
1594
|
+
nextErrors[path] = error;
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
if (match.index === index) {
|
|
1598
|
+
changed = true;
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
if (match.index > index) {
|
|
1602
|
+
const shiftedPath = shiftArrayItemIndex(path, arrayName, index + 1, -1);
|
|
1603
|
+
nextErrors[shiftedPath] = error;
|
|
1604
|
+
changed = true;
|
|
1605
|
+
} else {
|
|
1606
|
+
nextErrors[path] = error;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
if (changed) {
|
|
1610
|
+
this.errors = nextErrors;
|
|
1611
|
+
this.fieldErrorRenderer.applyErrors(this.registry.values(), this.errors);
|
|
1612
|
+
this.emitValidationEvent({
|
|
1613
|
+
type: "validation:end",
|
|
1614
|
+
errors: { ...this.errors },
|
|
1615
|
+
values: this.extractCurrentValues()
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
if (this.options.preserveUnmountedValues) {
|
|
1619
|
+
const nextPreserved = {};
|
|
1620
|
+
let pChanged = false;
|
|
1621
|
+
for (const [path, val] of Object.entries(this.preservedValuesStore)) {
|
|
1622
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1623
|
+
if (!match) {
|
|
1624
|
+
nextPreserved[path] = val;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (match.index === index) {
|
|
1628
|
+
pChanged = true;
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
if (match.index > index) {
|
|
1632
|
+
const shiftedPath = shiftArrayItemIndex(path, arrayName, index + 1, -1);
|
|
1633
|
+
nextPreserved[shiftedPath] = val;
|
|
1634
|
+
pChanged = true;
|
|
1635
|
+
} else {
|
|
1636
|
+
nextPreserved[path] = val;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (pChanged) {
|
|
1640
|
+
this.preservedValuesStore = nextPreserved;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
swapArrayItems(arrayName, indexA, indexB) {
|
|
1645
|
+
if (indexA === indexB) return;
|
|
1646
|
+
const nextErrors = {};
|
|
1647
|
+
let changed = false;
|
|
1648
|
+
for (const [path, error] of Object.entries(this.errors)) {
|
|
1649
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1650
|
+
if (!match) {
|
|
1651
|
+
nextErrors[path] = error;
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (match.index === indexA) {
|
|
1655
|
+
const newPath = remapArrayItemIndex(path, arrayName, indexA, indexB);
|
|
1656
|
+
nextErrors[newPath] = error;
|
|
1657
|
+
changed = true;
|
|
1658
|
+
} else if (match.index === indexB) {
|
|
1659
|
+
const newPath = remapArrayItemIndex(path, arrayName, indexB, indexA);
|
|
1660
|
+
nextErrors[newPath] = error;
|
|
1661
|
+
changed = true;
|
|
1662
|
+
} else {
|
|
1663
|
+
nextErrors[path] = error;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (changed) {
|
|
1667
|
+
this.errors = nextErrors;
|
|
1668
|
+
this.fieldErrorRenderer.applyErrors(this.registry.values(), this.errors);
|
|
1669
|
+
this.emitValidationEvent({
|
|
1670
|
+
type: "validation:end",
|
|
1671
|
+
errors: { ...this.errors },
|
|
1672
|
+
values: this.extractCurrentValues()
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (this.options.preserveUnmountedValues) {
|
|
1676
|
+
const nextPreserved = {};
|
|
1677
|
+
let pChanged = false;
|
|
1678
|
+
for (const [path, val] of Object.entries(this.preservedValuesStore)) {
|
|
1679
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1680
|
+
if (!match) {
|
|
1681
|
+
nextPreserved[path] = val;
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (match.index === indexA) {
|
|
1685
|
+
const newPath = remapArrayItemIndex(path, arrayName, indexA, indexB);
|
|
1686
|
+
nextPreserved[newPath] = val;
|
|
1687
|
+
pChanged = true;
|
|
1688
|
+
} else if (match.index === indexB) {
|
|
1689
|
+
const newPath = remapArrayItemIndex(path, arrayName, indexB, indexA);
|
|
1690
|
+
nextPreserved[newPath] = val;
|
|
1691
|
+
pChanged = true;
|
|
1692
|
+
} else {
|
|
1693
|
+
nextPreserved[path] = val;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (pChanged) {
|
|
1697
|
+
this.preservedValuesStore = nextPreserved;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
moveArrayItem(arrayName, fromIndex, toIndex) {
|
|
1702
|
+
if (fromIndex === toIndex) return;
|
|
1703
|
+
const nextErrors = {};
|
|
1704
|
+
let changed = false;
|
|
1705
|
+
for (const [path, error] of Object.entries(this.errors)) {
|
|
1706
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1707
|
+
if (!match) {
|
|
1708
|
+
nextErrors[path] = error;
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const idx = match.index;
|
|
1712
|
+
if (idx === fromIndex) {
|
|
1713
|
+
const newPath = remapArrayItemIndex(path, arrayName, fromIndex, toIndex);
|
|
1714
|
+
nextErrors[newPath] = error;
|
|
1715
|
+
changed = true;
|
|
1716
|
+
} else if (fromIndex < toIndex && idx > fromIndex && idx <= toIndex) {
|
|
1717
|
+
const newPath = remapArrayItemIndex(path, arrayName, idx, idx - 1);
|
|
1718
|
+
nextErrors[newPath] = error;
|
|
1719
|
+
changed = true;
|
|
1720
|
+
} else if (fromIndex > toIndex && idx >= toIndex && idx < fromIndex) {
|
|
1721
|
+
const newPath = remapArrayItemIndex(path, arrayName, idx, idx + 1);
|
|
1722
|
+
nextErrors[newPath] = error;
|
|
1723
|
+
changed = true;
|
|
1724
|
+
} else {
|
|
1725
|
+
nextErrors[path] = error;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (changed) {
|
|
1729
|
+
this.errors = nextErrors;
|
|
1730
|
+
this.fieldErrorRenderer.applyErrors(this.registry.values(), this.errors);
|
|
1731
|
+
this.emitValidationEvent({
|
|
1732
|
+
type: "validation:end",
|
|
1733
|
+
errors: { ...this.errors },
|
|
1734
|
+
values: this.extractCurrentValues()
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
if (this.options.preserveUnmountedValues) {
|
|
1738
|
+
const nextPreserved = {};
|
|
1739
|
+
let pChanged = false;
|
|
1740
|
+
for (const [path, val] of Object.entries(this.preservedValuesStore)) {
|
|
1741
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1742
|
+
if (!match) {
|
|
1743
|
+
nextPreserved[path] = val;
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
const idx = match.index;
|
|
1747
|
+
if (idx === fromIndex) {
|
|
1748
|
+
const newPath = remapArrayItemIndex(path, arrayName, fromIndex, toIndex);
|
|
1749
|
+
nextPreserved[newPath] = val;
|
|
1750
|
+
pChanged = true;
|
|
1751
|
+
} else if (fromIndex < toIndex && idx > fromIndex && idx <= toIndex) {
|
|
1752
|
+
const newPath = remapArrayItemIndex(path, arrayName, idx, idx - 1);
|
|
1753
|
+
nextPreserved[newPath] = val;
|
|
1754
|
+
pChanged = true;
|
|
1755
|
+
} else if (fromIndex > toIndex && idx >= toIndex && idx < fromIndex) {
|
|
1756
|
+
const newPath = remapArrayItemIndex(path, arrayName, idx, idx + 1);
|
|
1757
|
+
nextPreserved[newPath] = val;
|
|
1758
|
+
pChanged = true;
|
|
1759
|
+
} else {
|
|
1760
|
+
nextPreserved[path] = val;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (pChanged) {
|
|
1764
|
+
this.preservedValuesStore = nextPreserved;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
insertArrayItem(arrayName, index) {
|
|
1769
|
+
const nextErrors = {};
|
|
1770
|
+
let changed = false;
|
|
1771
|
+
for (const [path, error] of Object.entries(this.errors)) {
|
|
1772
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1773
|
+
if (!match) {
|
|
1774
|
+
nextErrors[path] = error;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (match.index >= index) {
|
|
1778
|
+
const shiftedPath = shiftArrayItemIndex(path, arrayName, index, 1);
|
|
1779
|
+
nextErrors[shiftedPath] = error;
|
|
1780
|
+
changed = true;
|
|
1781
|
+
} else {
|
|
1782
|
+
nextErrors[path] = error;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (changed) {
|
|
1786
|
+
this.errors = nextErrors;
|
|
1787
|
+
this.fieldErrorRenderer.applyErrors(this.registry.values(), this.errors);
|
|
1788
|
+
this.emitValidationEvent({
|
|
1789
|
+
type: "validation:end",
|
|
1790
|
+
errors: { ...this.errors },
|
|
1791
|
+
values: this.extractCurrentValues()
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
if (this.options.preserveUnmountedValues) {
|
|
1795
|
+
const nextPreserved = {};
|
|
1796
|
+
let pChanged = false;
|
|
1797
|
+
for (const [path, val] of Object.entries(this.preservedValuesStore)) {
|
|
1798
|
+
const match = matchArrayItemPath(path, arrayName);
|
|
1799
|
+
if (!match) {
|
|
1800
|
+
nextPreserved[path] = val;
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
if (match.index >= index) {
|
|
1804
|
+
const shiftedPath = shiftArrayItemIndex(path, arrayName, index, 1);
|
|
1805
|
+
nextPreserved[shiftedPath] = val;
|
|
1806
|
+
pChanged = true;
|
|
1807
|
+
} else {
|
|
1808
|
+
nextPreserved[path] = val;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (pChanged) {
|
|
1812
|
+
this.preservedValuesStore = nextPreserved;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
getFormState() {
|
|
1817
|
+
const fields = this.getFieldStates();
|
|
1818
|
+
return {
|
|
1819
|
+
dirty: fields.some((field) => field.dirty),
|
|
1820
|
+
touched: fields.some((field) => field.touched),
|
|
1821
|
+
valid: Object.keys(this.errors).length === 0,
|
|
1822
|
+
submitting: this.submitting,
|
|
1823
|
+
validating: this.validating || fields.some((field) => field.validating),
|
|
1824
|
+
errors: { ...this.errors },
|
|
1825
|
+
fields,
|
|
1826
|
+
values: this.extractCurrentValues()
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
ensureField(element) {
|
|
1830
|
+
if (this.registry.has(element.name)) {
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
this.registry.register(new FieldNode(element));
|
|
1834
|
+
const node = this.registry.get(element.name);
|
|
1835
|
+
node.register(this.getFieldValue(element.name));
|
|
1836
|
+
}
|
|
1837
|
+
getFieldValue(name) {
|
|
1838
|
+
if (!this.form) {
|
|
1839
|
+
if (this.options.preserveUnmountedValues) {
|
|
1840
|
+
return this.preservedValuesStore[name];
|
|
1841
|
+
}
|
|
1842
|
+
return void 0;
|
|
1843
|
+
}
|
|
1844
|
+
if (this.options.preserveUnmountedValues && !this.isSyncingFields) {
|
|
1845
|
+
this.syncFields();
|
|
1846
|
+
}
|
|
1847
|
+
let node = this.registry.get(name);
|
|
1848
|
+
if (!node) {
|
|
1849
|
+
const control = this.form.elements.namedItem(name);
|
|
1850
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLSelectElement || control instanceof HTMLTextAreaElement) {
|
|
1851
|
+
this.ensureField(control);
|
|
1852
|
+
node = this.registry.get(name);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (!node) {
|
|
1856
|
+
const normalizedTarget = normalizePath(name);
|
|
1857
|
+
for (const field of this.registry.values()) {
|
|
1858
|
+
if (normalizePath(field.name) === normalizedTarget) {
|
|
1859
|
+
node = field;
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (!node) {
|
|
1865
|
+
if (this.options.preserveUnmountedValues) {
|
|
1866
|
+
if (name in this.preservedValuesStore) {
|
|
1867
|
+
return this.preservedValuesStore[name];
|
|
1868
|
+
}
|
|
1869
|
+
const normalizedTarget = normalizePath(name);
|
|
1870
|
+
for (const [key, val] of Object.entries(this.preservedValuesStore)) {
|
|
1871
|
+
if (normalizePath(key) === normalizedTarget) {
|
|
1872
|
+
return val;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (this.options.nesting === "nested") {
|
|
1877
|
+
const nested = this.extractCurrentValues();
|
|
1878
|
+
const resolved = getByPath(nested, name);
|
|
1879
|
+
if (resolved !== void 0) {
|
|
1880
|
+
return resolved;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return void 0;
|
|
1884
|
+
}
|
|
1885
|
+
if (node.element instanceof HTMLInputElement && node.element.type === "radio") {
|
|
1886
|
+
const item = this.form.elements.namedItem(name);
|
|
1887
|
+
if (item) {
|
|
1888
|
+
if ("value" in item && typeof item.value === "string" && item.value !== "") {
|
|
1889
|
+
return item.value;
|
|
1890
|
+
}
|
|
1891
|
+
if ("length" in item) {
|
|
1892
|
+
for (const el of Array.from(item)) {
|
|
1893
|
+
if (el instanceof HTMLInputElement && el.checked) {
|
|
1894
|
+
return el.value;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return node.element.checked ? node.element.value : void 0;
|
|
1900
|
+
}
|
|
1901
|
+
if (node.element instanceof HTMLInputElement && node.element.type === "checkbox") {
|
|
1902
|
+
const item = this.form.elements.namedItem(name);
|
|
1903
|
+
if (item && ("length" in item || item instanceof RadioNodeList) && item.length > 1) {
|
|
1904
|
+
const checkedValues = [];
|
|
1905
|
+
for (const el of Array.from(item)) {
|
|
1906
|
+
if (el instanceof HTMLInputElement && el.checked) {
|
|
1907
|
+
checkedValues.push(el.value || "on");
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
return checkedValues;
|
|
1911
|
+
}
|
|
1912
|
+
return node.element.checked;
|
|
1913
|
+
}
|
|
1914
|
+
return node.value ?? void 0;
|
|
1915
|
+
}
|
|
1916
|
+
getValidationFields() {
|
|
1917
|
+
return this.registry.values().map((field) => this.toValidationField(field));
|
|
1918
|
+
}
|
|
1919
|
+
toValidationField(field) {
|
|
1920
|
+
return {
|
|
1921
|
+
name: field.name,
|
|
1922
|
+
validationMessage: field.element.validationMessage
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
syncFields() {
|
|
1926
|
+
if (!this.form || this.isSyncingFields) {
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
this.isSyncingFields = true;
|
|
1930
|
+
try {
|
|
1931
|
+
const previousFields = this.registry.values();
|
|
1932
|
+
const previousFieldMap = new Map(
|
|
1933
|
+
previousFields.map((field) => [field.name, field])
|
|
1934
|
+
);
|
|
1935
|
+
const activeControlNames = /* @__PURE__ */ new Set();
|
|
1936
|
+
for (const control of Array.from(this.form.elements)) {
|
|
1937
|
+
if ((control instanceof HTMLInputElement || control instanceof HTMLSelectElement || control instanceof HTMLTextAreaElement) && control.name) {
|
|
1938
|
+
activeControlNames.add(control.name);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
for (const field of previousFields) {
|
|
1942
|
+
if (!activeControlNames.has(field.name)) {
|
|
1943
|
+
if (this.options.preserveUnmountedValues) {
|
|
1944
|
+
const val = field.value;
|
|
1945
|
+
if (field.name !== "__proto__" && field.name !== "constructor" && field.name !== "prototype") {
|
|
1946
|
+
if (val !== void 0) {
|
|
1947
|
+
this.preservedValuesStore[field.name] = val;
|
|
1948
|
+
} else {
|
|
1949
|
+
delete this.preservedValuesStore[field.name];
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
this.fieldErrorRenderer.applyErrors([field], {});
|
|
1954
|
+
delete this.errors[field.name];
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
this.discovery.discover(this.form, this.registry);
|
|
1958
|
+
const values = this.extractCurrentValues();
|
|
1959
|
+
for (const field of this.registry.values()) {
|
|
1960
|
+
if (!previousFieldMap.has(field.name) || previousFieldMap.get(field.name) !== field) {
|
|
1961
|
+
field.register(values[field.name]);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
} finally {
|
|
1965
|
+
this.isSyncingFields = false;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
handleMutation = (mutations) => {
|
|
1969
|
+
if (!mutations.some(isRuntimeMutationRelevant)) {
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
this.syncFields();
|
|
1973
|
+
};
|
|
1974
|
+
applyFieldValidationResult(targetField, result, values) {
|
|
1975
|
+
const issueMap = result.success ? {} : issuesToErrors(result.issues);
|
|
1976
|
+
const targetName = targetField.name;
|
|
1977
|
+
const targetIssue = resolveFieldError(issueMap, targetName);
|
|
1978
|
+
if (targetIssue) {
|
|
1979
|
+
this.errors[targetName] = targetIssue;
|
|
1980
|
+
targetField.setError(targetIssue);
|
|
1981
|
+
} else {
|
|
1982
|
+
delete this.errors[targetName];
|
|
1983
|
+
const normalizedTarget = normalizePath(targetName);
|
|
1984
|
+
if (this.errors[normalizedTarget]) {
|
|
1985
|
+
delete this.errors[normalizedTarget];
|
|
1986
|
+
}
|
|
1987
|
+
targetField.setError(void 0);
|
|
1988
|
+
}
|
|
1989
|
+
for (const otherField of this.registry.values()) {
|
|
1990
|
+
if (otherField === targetField) continue;
|
|
1991
|
+
const name = otherField.name;
|
|
1992
|
+
const hadError = Boolean(resolveFieldError(this.errors, name));
|
|
1993
|
+
const isTouched = otherField.isTouched;
|
|
1994
|
+
if (hadError || isTouched) {
|
|
1995
|
+
const issue = resolveFieldError(issueMap, name);
|
|
1996
|
+
if (issue) {
|
|
1997
|
+
this.errors[name] = issue;
|
|
1998
|
+
otherField.setError(issue);
|
|
1999
|
+
} else {
|
|
2000
|
+
delete this.errors[name];
|
|
2001
|
+
const normalizedOther = normalizePath(name);
|
|
2002
|
+
if (this.errors[normalizedOther]) {
|
|
2003
|
+
delete this.errors[normalizedOther];
|
|
2004
|
+
}
|
|
2005
|
+
otherField.setError(void 0);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
this.fieldErrorRenderer.normalizeFields(this.registry.values());
|
|
2010
|
+
this.applyNativeErrors();
|
|
2011
|
+
this.emitValidationEvent({
|
|
2012
|
+
type: "validation:end",
|
|
2013
|
+
errors: { ...this.errors },
|
|
2014
|
+
values
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
applyValidationResult(result, values) {
|
|
2018
|
+
this.fieldErrorRenderer.normalizeFields(this.registry.values());
|
|
2019
|
+
this.errors = result.success ? {} : issuesToErrors(result.issues);
|
|
2020
|
+
for (const field of this.registry.values()) {
|
|
2021
|
+
const error = resolveFieldError(this.errors, field.name);
|
|
2022
|
+
if (error && this.errors[field.name] === void 0) {
|
|
2023
|
+
this.errors[field.name] = error;
|
|
2024
|
+
}
|
|
2025
|
+
field.setError(error);
|
|
2026
|
+
}
|
|
2027
|
+
this.applyNativeErrors();
|
|
2028
|
+
this.emitValidationEvent({
|
|
2029
|
+
type: "validation:end",
|
|
2030
|
+
errors: this.errors,
|
|
2031
|
+
values
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
applyNativeErrors() {
|
|
2035
|
+
const observer = this.observer;
|
|
2036
|
+
if (observer) {
|
|
2037
|
+
observer.disconnect();
|
|
2038
|
+
}
|
|
2039
|
+
const renderDomErrors = this.options.renderDomErrors ?? true;
|
|
2040
|
+
this.fieldErrorRenderer.applyErrors(
|
|
2041
|
+
this.registry.values(),
|
|
2042
|
+
this.errors,
|
|
2043
|
+
renderDomErrors
|
|
2044
|
+
);
|
|
2045
|
+
if (observer && this.form) {
|
|
2046
|
+
observer.observe(this.form, runtimeMutationObserverOptions);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
function issuesToErrors(issues) {
|
|
2051
|
+
const errors = {};
|
|
2052
|
+
for (const issue of issues) {
|
|
2053
|
+
errors[issue.name] ??= issue.message;
|
|
2054
|
+
}
|
|
2055
|
+
return errors;
|
|
2056
|
+
}
|
|
2057
|
+
function resolveFieldError(errors, name) {
|
|
2058
|
+
if (errors[name] !== void 0) {
|
|
2059
|
+
return errors[name];
|
|
2060
|
+
}
|
|
2061
|
+
const normalized = normalizePath(name);
|
|
2062
|
+
if (errors[normalized] !== void 0) {
|
|
2063
|
+
return errors[normalized];
|
|
2064
|
+
}
|
|
2065
|
+
for (const [key, msg] of Object.entries(errors)) {
|
|
2066
|
+
if (normalizePath(key) === normalized) {
|
|
2067
|
+
return msg;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return void 0;
|
|
2071
|
+
}
|
|
2072
|
+
function toNativeFormElement(target) {
|
|
2073
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement) {
|
|
2074
|
+
return target;
|
|
2075
|
+
}
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2079
|
+
0 && (module.exports = {
|
|
2080
|
+
FieldNode,
|
|
2081
|
+
Runtime,
|
|
2082
|
+
Serializer,
|
|
2083
|
+
arrayItemPath,
|
|
2084
|
+
createValidationPipeline,
|
|
2085
|
+
createValidator,
|
|
2086
|
+
formPath,
|
|
2087
|
+
getByPath,
|
|
2088
|
+
isArrayItemPath,
|
|
2089
|
+
matchArrayItemPath,
|
|
2090
|
+
normalizePath,
|
|
2091
|
+
parseArrayItemPath,
|
|
2092
|
+
remapArrayItemIndex,
|
|
2093
|
+
setByPath,
|
|
2094
|
+
shiftArrayItemIndex,
|
|
2095
|
+
splitPath,
|
|
2096
|
+
toNestedValues
|
|
2097
|
+
});
|