@ereo/forms 0.1.23
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/dist/a11y.d.ts +52 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/accessibility.d.ts +152 -0
- package/dist/accessibility.d.ts.map +1 -0
- package/dist/action-integration.d.ts +104 -0
- package/dist/action-integration.d.ts.map +1 -0
- package/dist/action.d.ts +33 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/array-field.d.ts +49 -0
- package/dist/array-field.d.ts.map +1 -0
- package/dist/components.d.ts +8 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/composition.d.ts +4 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/field.d.ts +88 -0
- package/dist/field.d.ts.map +1 -0
- package/dist/form.d.ts +59 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/hooks.d.ts +13 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2945 -0
- package/dist/proxy.d.ts +3 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/schema.d.ts +31 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/store.d.ts +69 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/validation-engine.d.ts +33 -0
- package/dist/validation-engine.d.ts.map +1 -0
- package/dist/validation.d.ts +241 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validators.d.ts +51 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/wizard.d.ts +52 -0
- package/dist/wizard.d.ts.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2945 @@
|
|
|
1
|
+
// src/store.ts
|
|
2
|
+
import { signal as signal2, batch } from "@ereo/state";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
function parsePath(path) {
|
|
6
|
+
if (!path)
|
|
7
|
+
return [];
|
|
8
|
+
const segments = [];
|
|
9
|
+
let current = "";
|
|
10
|
+
for (let i = 0;i < path.length; i++) {
|
|
11
|
+
const char = path[i];
|
|
12
|
+
if (char === ".") {
|
|
13
|
+
if (current) {
|
|
14
|
+
segments.push(isIndex(current) ? parseInt(current, 10) : current);
|
|
15
|
+
current = "";
|
|
16
|
+
}
|
|
17
|
+
} else if (char === "[") {
|
|
18
|
+
if (current) {
|
|
19
|
+
segments.push(isIndex(current) ? parseInt(current, 10) : current);
|
|
20
|
+
current = "";
|
|
21
|
+
}
|
|
22
|
+
const closeBracket = path.indexOf("]", i);
|
|
23
|
+
if (closeBracket !== -1) {
|
|
24
|
+
const indexStr = path.slice(i + 1, closeBracket);
|
|
25
|
+
const index = parseInt(indexStr, 10);
|
|
26
|
+
if (!isNaN(index)) {
|
|
27
|
+
segments.push(index);
|
|
28
|
+
} else if (indexStr) {
|
|
29
|
+
segments.push(indexStr);
|
|
30
|
+
}
|
|
31
|
+
i = closeBracket;
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
current += char;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (current) {
|
|
38
|
+
segments.push(isIndex(current) ? parseInt(current, 10) : current);
|
|
39
|
+
}
|
|
40
|
+
return segments;
|
|
41
|
+
}
|
|
42
|
+
function isIndex(str) {
|
|
43
|
+
return /^\d+$/.test(str);
|
|
44
|
+
}
|
|
45
|
+
function getPath(obj, path) {
|
|
46
|
+
if (!path)
|
|
47
|
+
return obj;
|
|
48
|
+
const segments = parsePath(path);
|
|
49
|
+
let current = obj;
|
|
50
|
+
for (const segment of segments) {
|
|
51
|
+
if (current == null)
|
|
52
|
+
return;
|
|
53
|
+
current = current[segment];
|
|
54
|
+
}
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
function setPath(obj, path, value) {
|
|
58
|
+
const segments = parsePath(path);
|
|
59
|
+
if (segments.length === 0)
|
|
60
|
+
return value;
|
|
61
|
+
return setPathRecursive(obj, segments, 0, value);
|
|
62
|
+
}
|
|
63
|
+
function setPathRecursive(current, segments, index, value) {
|
|
64
|
+
const segment = segments[index];
|
|
65
|
+
if (current !== null && current !== undefined && typeof current !== "object") {
|
|
66
|
+
const nextSegment2 = segments[index + 1];
|
|
67
|
+
current = typeof nextSegment2 === "number" ? [] : {};
|
|
68
|
+
}
|
|
69
|
+
if (index === segments.length - 1) {
|
|
70
|
+
if (Array.isArray(current)) {
|
|
71
|
+
const copy = [...current];
|
|
72
|
+
copy[segment] = value;
|
|
73
|
+
return copy;
|
|
74
|
+
}
|
|
75
|
+
return { ...current ?? {}, [segment]: value };
|
|
76
|
+
}
|
|
77
|
+
const next = current?.[segment];
|
|
78
|
+
const nextSegment = segments[index + 1];
|
|
79
|
+
const nextIsArray = typeof nextSegment === "number";
|
|
80
|
+
const nextValue = next ?? (nextIsArray ? [] : {});
|
|
81
|
+
const updated = setPathRecursive(nextValue, segments, index + 1, value);
|
|
82
|
+
if (Array.isArray(current)) {
|
|
83
|
+
const copy = [...current];
|
|
84
|
+
copy[segment] = updated;
|
|
85
|
+
return copy;
|
|
86
|
+
}
|
|
87
|
+
return { ...current ?? {}, [segment]: updated };
|
|
88
|
+
}
|
|
89
|
+
function deepClone(obj) {
|
|
90
|
+
if (obj === null || typeof obj !== "object")
|
|
91
|
+
return obj;
|
|
92
|
+
if (typeof structuredClone === "function") {
|
|
93
|
+
return structuredClone(obj);
|
|
94
|
+
}
|
|
95
|
+
if (obj instanceof Date)
|
|
96
|
+
return new Date(obj.getTime());
|
|
97
|
+
if (obj instanceof RegExp)
|
|
98
|
+
return new RegExp(obj.source, obj.flags);
|
|
99
|
+
if (obj instanceof Map) {
|
|
100
|
+
const map = new Map;
|
|
101
|
+
for (const [k, v] of obj)
|
|
102
|
+
map.set(deepClone(k), deepClone(v));
|
|
103
|
+
return map;
|
|
104
|
+
}
|
|
105
|
+
if (obj instanceof Set) {
|
|
106
|
+
const set = new Set;
|
|
107
|
+
for (const v of obj)
|
|
108
|
+
set.add(deepClone(v));
|
|
109
|
+
return set;
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(obj)) {
|
|
112
|
+
return obj.map((item) => deepClone(item));
|
|
113
|
+
}
|
|
114
|
+
const result = {};
|
|
115
|
+
for (const key of Object.keys(obj)) {
|
|
116
|
+
result[key] = deepClone(obj[key]);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function deepEqual(a, b) {
|
|
121
|
+
if (a === b)
|
|
122
|
+
return true;
|
|
123
|
+
if (a === null || b === null)
|
|
124
|
+
return false;
|
|
125
|
+
if (typeof a !== typeof b)
|
|
126
|
+
return false;
|
|
127
|
+
if (typeof a !== "object")
|
|
128
|
+
return false;
|
|
129
|
+
if (a instanceof Date && b instanceof Date) {
|
|
130
|
+
return a.getTime() === b.getTime();
|
|
131
|
+
}
|
|
132
|
+
if (a instanceof Date || b instanceof Date)
|
|
133
|
+
return false;
|
|
134
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
135
|
+
return a.source === b.source && a.flags === b.flags;
|
|
136
|
+
}
|
|
137
|
+
if (a instanceof RegExp || b instanceof RegExp)
|
|
138
|
+
return false;
|
|
139
|
+
if (a instanceof Map && b instanceof Map) {
|
|
140
|
+
if (a.size !== b.size)
|
|
141
|
+
return false;
|
|
142
|
+
for (const [k, v] of a) {
|
|
143
|
+
if (!b.has(k) || !deepEqual(v, b.get(k)))
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (a instanceof Set && b instanceof Set) {
|
|
149
|
+
if (a.size !== b.size)
|
|
150
|
+
return false;
|
|
151
|
+
for (const v of a) {
|
|
152
|
+
if (!b.has(v))
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
158
|
+
return false;
|
|
159
|
+
if (Array.isArray(a)) {
|
|
160
|
+
if (!Array.isArray(b))
|
|
161
|
+
return false;
|
|
162
|
+
if (a.length !== b.length)
|
|
163
|
+
return false;
|
|
164
|
+
for (let i = 0;i < a.length; i++) {
|
|
165
|
+
if (!deepEqual(a[i], b[i]))
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const keysA = Object.keys(a);
|
|
171
|
+
const keysB = Object.keys(b);
|
|
172
|
+
if (keysA.length !== keysB.length)
|
|
173
|
+
return false;
|
|
174
|
+
for (const key of keysA) {
|
|
175
|
+
if (!deepEqual(a[key], b[key])) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function flattenToPaths(obj, prefix = "") {
|
|
182
|
+
const result = new Map;
|
|
183
|
+
if (obj === null || typeof obj !== "object") {
|
|
184
|
+
if (prefix)
|
|
185
|
+
result.set(prefix, obj);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(obj)) {
|
|
189
|
+
for (let i = 0;i < obj.length; i++) {
|
|
190
|
+
const path = prefix ? `${prefix}.${i}` : `${i}`;
|
|
191
|
+
const nested = flattenToPaths(obj[i], path);
|
|
192
|
+
for (const [k, v] of nested) {
|
|
193
|
+
result.set(k, v);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (prefix)
|
|
197
|
+
result.set(prefix, obj);
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
if (prefix)
|
|
201
|
+
result.set(prefix, obj);
|
|
202
|
+
for (const key of Object.keys(obj)) {
|
|
203
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
204
|
+
const value = obj[key];
|
|
205
|
+
if (value !== null && typeof value === "object") {
|
|
206
|
+
const nested = flattenToPaths(value, path);
|
|
207
|
+
for (const [k, v] of nested) {
|
|
208
|
+
result.set(k, v);
|
|
209
|
+
}
|
|
210
|
+
result.set(path, value);
|
|
211
|
+
} else {
|
|
212
|
+
result.set(path, value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/proxy.ts
|
|
219
|
+
var PROXY_MARKER = Symbol("ereo-form-proxy");
|
|
220
|
+
function createValuesProxy(store, basePath = "", proxyCache) {
|
|
221
|
+
const cache = proxyCache ?? new Map;
|
|
222
|
+
if (basePath && cache.has(basePath)) {
|
|
223
|
+
return cache.get(basePath);
|
|
224
|
+
}
|
|
225
|
+
const proxy = new Proxy({}, {
|
|
226
|
+
get(_target, prop, _receiver) {
|
|
227
|
+
if (prop === PROXY_MARKER)
|
|
228
|
+
return true;
|
|
229
|
+
if (typeof prop === "symbol")
|
|
230
|
+
return;
|
|
231
|
+
const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
232
|
+
const value = store.getValue(fullPath);
|
|
233
|
+
if (value !== null && typeof value === "object") {
|
|
234
|
+
return createValuesProxy(store, fullPath, cache);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
},
|
|
238
|
+
set(_target, prop, value) {
|
|
239
|
+
if (typeof prop === "symbol")
|
|
240
|
+
return true;
|
|
241
|
+
const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
242
|
+
store.setValue(fullPath, value);
|
|
243
|
+
return true;
|
|
244
|
+
},
|
|
245
|
+
has(_target, prop) {
|
|
246
|
+
if (prop === PROXY_MARKER)
|
|
247
|
+
return true;
|
|
248
|
+
if (typeof prop === "symbol")
|
|
249
|
+
return false;
|
|
250
|
+
const obj = basePath ? store.getValue(basePath) : store.getValues();
|
|
251
|
+
if (obj !== null && typeof obj === "object") {
|
|
252
|
+
return String(prop) in obj;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
},
|
|
256
|
+
ownKeys() {
|
|
257
|
+
const obj = basePath ? store.getValue(basePath) : store.getValues();
|
|
258
|
+
if (obj !== null && typeof obj === "object") {
|
|
259
|
+
return Object.keys(obj);
|
|
260
|
+
}
|
|
261
|
+
return [];
|
|
262
|
+
},
|
|
263
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
264
|
+
if (typeof prop === "symbol")
|
|
265
|
+
return;
|
|
266
|
+
const obj = basePath ? store.getValue(basePath) : store.getValues();
|
|
267
|
+
if (obj !== null && typeof obj === "object" && String(prop) in obj) {
|
|
268
|
+
const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
269
|
+
return {
|
|
270
|
+
configurable: true,
|
|
271
|
+
enumerable: true,
|
|
272
|
+
writable: true,
|
|
273
|
+
value: store.getValue(fullPath)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
if (basePath) {
|
|
280
|
+
cache.set(basePath, proxy);
|
|
281
|
+
}
|
|
282
|
+
return proxy;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/validation-engine.ts
|
|
286
|
+
import { signal } from "@ereo/state";
|
|
287
|
+
|
|
288
|
+
class ValidationEngine {
|
|
289
|
+
_store;
|
|
290
|
+
_fieldValidations = new Map;
|
|
291
|
+
_abortControllers = new Map;
|
|
292
|
+
_debounceTimers = new Map;
|
|
293
|
+
_validatingFields = new Set;
|
|
294
|
+
_validatingSignals = new Map;
|
|
295
|
+
_fieldGenerations = new Map;
|
|
296
|
+
_validateAllController = null;
|
|
297
|
+
constructor(store) {
|
|
298
|
+
this._store = store;
|
|
299
|
+
const { validators } = store.config;
|
|
300
|
+
if (validators) {
|
|
301
|
+
for (const [path, rules] of Object.entries(validators)) {
|
|
302
|
+
if (rules) {
|
|
303
|
+
const arr = Array.isArray(rules) ? rules : [rules];
|
|
304
|
+
this.registerFieldValidators(path, arr, store.config.validateOn);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
registerFieldValidators(path, validators, explicitTrigger) {
|
|
310
|
+
const derivedTrigger = explicitTrigger ?? this._deriveValidateOn(validators);
|
|
311
|
+
this._fieldValidations.set(path, {
|
|
312
|
+
validators,
|
|
313
|
+
validateOn: explicitTrigger,
|
|
314
|
+
derivedTrigger
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
_deriveValidateOn(validators) {
|
|
318
|
+
const hasAsync = validators.some((v) => v._isAsync);
|
|
319
|
+
if (hasAsync)
|
|
320
|
+
return "change";
|
|
321
|
+
const allRequired = validators.every((v) => v._isRequired);
|
|
322
|
+
if (allRequired)
|
|
323
|
+
return "blur";
|
|
324
|
+
return "blur";
|
|
325
|
+
}
|
|
326
|
+
onFieldChange(path) {
|
|
327
|
+
const validation = this._fieldValidations.get(path);
|
|
328
|
+
if (!validation)
|
|
329
|
+
return;
|
|
330
|
+
const trigger = validation.validateOn ?? validation.derivedTrigger;
|
|
331
|
+
if (trigger !== "change")
|
|
332
|
+
return;
|
|
333
|
+
const debounceMs = this._getDebounceMs(validation.validators);
|
|
334
|
+
if (debounceMs > 0) {
|
|
335
|
+
this._debounceValidation(path, debounceMs);
|
|
336
|
+
} else {
|
|
337
|
+
this._runFieldValidation(path);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
onFieldBlur(path) {
|
|
341
|
+
const validation = this._fieldValidations.get(path);
|
|
342
|
+
if (!validation)
|
|
343
|
+
return;
|
|
344
|
+
const trigger = validation.validateOn ?? validation.derivedTrigger;
|
|
345
|
+
if (trigger === "submit")
|
|
346
|
+
return;
|
|
347
|
+
const timer = this._debounceTimers.get(path);
|
|
348
|
+
if (timer) {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
this._debounceTimers.delete(path);
|
|
351
|
+
}
|
|
352
|
+
this._runFieldValidation(path);
|
|
353
|
+
}
|
|
354
|
+
isFieldValidating(path) {
|
|
355
|
+
return this._validatingFields.has(path);
|
|
356
|
+
}
|
|
357
|
+
getFieldValidatingSignal(path) {
|
|
358
|
+
let sig = this._validatingSignals.get(path);
|
|
359
|
+
if (!sig) {
|
|
360
|
+
sig = signal(false);
|
|
361
|
+
this._validatingSignals.set(path, sig);
|
|
362
|
+
}
|
|
363
|
+
return sig;
|
|
364
|
+
}
|
|
365
|
+
_setFieldValidating(path, validating) {
|
|
366
|
+
if (validating) {
|
|
367
|
+
this._validatingFields.add(path);
|
|
368
|
+
} else {
|
|
369
|
+
this._validatingFields.delete(path);
|
|
370
|
+
}
|
|
371
|
+
const sig = this._validatingSignals.get(path);
|
|
372
|
+
if (sig)
|
|
373
|
+
sig.set(validating);
|
|
374
|
+
}
|
|
375
|
+
async validateField(path) {
|
|
376
|
+
const validation = this._fieldValidations.get(path);
|
|
377
|
+
if (!validation)
|
|
378
|
+
return [];
|
|
379
|
+
this._cancelFieldValidation(path);
|
|
380
|
+
const controller = new AbortController;
|
|
381
|
+
this._abortControllers.set(path, controller);
|
|
382
|
+
const generation = (this._fieldGenerations.get(path) ?? 0) + 1;
|
|
383
|
+
this._fieldGenerations.set(path, generation);
|
|
384
|
+
this._setFieldValidating(path, true);
|
|
385
|
+
const errors = [];
|
|
386
|
+
const value = this._store.getValue(path);
|
|
387
|
+
const context = this._createContext(controller.signal);
|
|
388
|
+
try {
|
|
389
|
+
for (const validator of validation.validators) {
|
|
390
|
+
if (controller.signal.aborted)
|
|
391
|
+
break;
|
|
392
|
+
const result = await validator(value, context);
|
|
393
|
+
if (result) {
|
|
394
|
+
errors.push(result);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
if (this._abortControllers.get(path) === controller) {
|
|
399
|
+
this._setFieldValidating(path, false);
|
|
400
|
+
this._abortControllers.delete(path);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!controller.signal.aborted && this._fieldGenerations.get(path) === generation) {
|
|
404
|
+
this._store.setErrors(path, errors);
|
|
405
|
+
return errors;
|
|
406
|
+
}
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
async validateFields(paths) {
|
|
410
|
+
const allErrors = {};
|
|
411
|
+
let hasErrors = false;
|
|
412
|
+
await Promise.all(paths.map(async (path) => {
|
|
413
|
+
const errors = await this.validateField(path);
|
|
414
|
+
if (errors.length > 0) {
|
|
415
|
+
allErrors[path] = errors;
|
|
416
|
+
hasErrors = true;
|
|
417
|
+
}
|
|
418
|
+
}));
|
|
419
|
+
return { success: !hasErrors, errors: hasErrors ? allErrors : undefined };
|
|
420
|
+
}
|
|
421
|
+
async validateAll() {
|
|
422
|
+
if (this._validateAllController) {
|
|
423
|
+
this._validateAllController.abort();
|
|
424
|
+
}
|
|
425
|
+
const controller = new AbortController;
|
|
426
|
+
this._validateAllController = controller;
|
|
427
|
+
const allErrors = {};
|
|
428
|
+
let hasErrors = false;
|
|
429
|
+
const schema = this._store.config.schema;
|
|
430
|
+
if (schema) {
|
|
431
|
+
const schemaResult = await this._validateSchema(schema);
|
|
432
|
+
if (controller.signal.aborted)
|
|
433
|
+
return { success: true };
|
|
434
|
+
if (!schemaResult.success && schemaResult.errors) {
|
|
435
|
+
for (const [path, errors] of Object.entries(schemaResult.errors)) {
|
|
436
|
+
allErrors[path] = errors;
|
|
437
|
+
hasErrors = true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const fieldPaths = Array.from(this._fieldValidations.keys());
|
|
442
|
+
await Promise.all(fieldPaths.map(async (path) => {
|
|
443
|
+
if (controller.signal.aborted)
|
|
444
|
+
return;
|
|
445
|
+
const validation = this._fieldValidations.get(path);
|
|
446
|
+
if (!validation)
|
|
447
|
+
return;
|
|
448
|
+
const value = this._store.getValue(path);
|
|
449
|
+
const context = this._createContext(controller.signal);
|
|
450
|
+
const errors = [];
|
|
451
|
+
for (const validator of validation.validators) {
|
|
452
|
+
if (controller.signal.aborted)
|
|
453
|
+
break;
|
|
454
|
+
const result = await validator(value, context);
|
|
455
|
+
if (result)
|
|
456
|
+
errors.push(result);
|
|
457
|
+
}
|
|
458
|
+
if (errors.length > 0 && !controller.signal.aborted) {
|
|
459
|
+
if (allErrors[path]) {
|
|
460
|
+
allErrors[path] = [...allErrors[path], ...errors];
|
|
461
|
+
} else {
|
|
462
|
+
allErrors[path] = errors;
|
|
463
|
+
}
|
|
464
|
+
hasErrors = true;
|
|
465
|
+
}
|
|
466
|
+
}));
|
|
467
|
+
if (controller.signal.aborted)
|
|
468
|
+
return { success: true };
|
|
469
|
+
if (this._validateAllController === controller) {
|
|
470
|
+
this._validateAllController = null;
|
|
471
|
+
}
|
|
472
|
+
this._store.clearErrors();
|
|
473
|
+
for (const [path, errors] of Object.entries(allErrors)) {
|
|
474
|
+
this._store.setErrors(path, errors);
|
|
475
|
+
}
|
|
476
|
+
return { success: !hasErrors, errors: hasErrors ? allErrors : undefined };
|
|
477
|
+
}
|
|
478
|
+
async _validateSchema(schema) {
|
|
479
|
+
const values = this._store._getCurrentValues();
|
|
480
|
+
if (schema.safeParse) {
|
|
481
|
+
const result = schema.safeParse(values);
|
|
482
|
+
if (result.success) {
|
|
483
|
+
return { success: true };
|
|
484
|
+
}
|
|
485
|
+
const errors = {};
|
|
486
|
+
if (result.error?.issues) {
|
|
487
|
+
for (const issue of result.error.issues) {
|
|
488
|
+
const path = issue.path.join(".");
|
|
489
|
+
if (!errors[path])
|
|
490
|
+
errors[path] = [];
|
|
491
|
+
errors[path].push(issue.message);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { success: false, errors };
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
schema.parse(values);
|
|
498
|
+
return { success: true };
|
|
499
|
+
} catch (e) {
|
|
500
|
+
if (e?.issues) {
|
|
501
|
+
const errors = {};
|
|
502
|
+
for (const issue of e.issues) {
|
|
503
|
+
const path = issue.path?.join(".") ?? "";
|
|
504
|
+
if (!errors[path])
|
|
505
|
+
errors[path] = [];
|
|
506
|
+
errors[path].push(issue.message);
|
|
507
|
+
}
|
|
508
|
+
return { success: false, errors };
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
errors: { "": [e?.message ?? "Validation failed"] }
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
unregisterField(path) {
|
|
517
|
+
this._cancelFieldValidation(path);
|
|
518
|
+
this._fieldValidations.delete(path);
|
|
519
|
+
this._validatingSignals.delete(path);
|
|
520
|
+
this._fieldGenerations.delete(path);
|
|
521
|
+
}
|
|
522
|
+
dispose() {
|
|
523
|
+
for (const timer of this._debounceTimers.values()) {
|
|
524
|
+
clearTimeout(timer);
|
|
525
|
+
}
|
|
526
|
+
this._debounceTimers.clear();
|
|
527
|
+
for (const controller of this._abortControllers.values()) {
|
|
528
|
+
controller.abort();
|
|
529
|
+
}
|
|
530
|
+
this._abortControllers.clear();
|
|
531
|
+
this._validatingFields.clear();
|
|
532
|
+
this._validatingSignals.clear();
|
|
533
|
+
this._fieldGenerations.clear();
|
|
534
|
+
if (this._validateAllController) {
|
|
535
|
+
this._validateAllController.abort();
|
|
536
|
+
this._validateAllController = null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
_createContext(contextSignal) {
|
|
540
|
+
return {
|
|
541
|
+
getValue: (path) => this._store.getValue(path),
|
|
542
|
+
getValues: () => this._store._getCurrentValues(),
|
|
543
|
+
signal: contextSignal
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
_getDebounceMs(validators) {
|
|
547
|
+
for (const v of validators) {
|
|
548
|
+
if (v._debounce)
|
|
549
|
+
return v._debounce;
|
|
550
|
+
if (v._isAsync)
|
|
551
|
+
return 300;
|
|
552
|
+
}
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
_debounceValidation(path, ms) {
|
|
556
|
+
const existing = this._debounceTimers.get(path);
|
|
557
|
+
if (existing)
|
|
558
|
+
clearTimeout(existing);
|
|
559
|
+
this._debounceTimers.set(path, setTimeout(() => {
|
|
560
|
+
this._debounceTimers.delete(path);
|
|
561
|
+
this._runFieldValidation(path);
|
|
562
|
+
}, ms));
|
|
563
|
+
}
|
|
564
|
+
_runFieldValidation(path) {
|
|
565
|
+
this.validateField(path).catch(() => {});
|
|
566
|
+
}
|
|
567
|
+
_cancelFieldValidation(path) {
|
|
568
|
+
const controller = this._abortControllers.get(path);
|
|
569
|
+
if (controller) {
|
|
570
|
+
controller.abort();
|
|
571
|
+
this._abortControllers.delete(path);
|
|
572
|
+
}
|
|
573
|
+
this._setFieldValidating(path, false);
|
|
574
|
+
const timer = this._debounceTimers.get(path);
|
|
575
|
+
if (timer) {
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
this._debounceTimers.delete(path);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/store.ts
|
|
583
|
+
class FormStore {
|
|
584
|
+
config;
|
|
585
|
+
_signals = new Map;
|
|
586
|
+
_errorSignals = new Map;
|
|
587
|
+
_formErrors;
|
|
588
|
+
_baseline;
|
|
589
|
+
_touchedSet = new Set;
|
|
590
|
+
_dirtySet = new Set;
|
|
591
|
+
_fieldRefs = new Map;
|
|
592
|
+
_fieldOptions = new Map;
|
|
593
|
+
_subscribers = new Set;
|
|
594
|
+
_watchers = new Map;
|
|
595
|
+
_submitAbort = null;
|
|
596
|
+
_submitGeneration = 0;
|
|
597
|
+
_validationEngine;
|
|
598
|
+
isValid;
|
|
599
|
+
isDirty;
|
|
600
|
+
isSubmitting;
|
|
601
|
+
submitState;
|
|
602
|
+
submitCount;
|
|
603
|
+
values;
|
|
604
|
+
constructor(config) {
|
|
605
|
+
this.config = config;
|
|
606
|
+
this._baseline = deepClone(config.defaultValues);
|
|
607
|
+
const paths = flattenToPaths(config.defaultValues);
|
|
608
|
+
for (const [path, value] of paths) {
|
|
609
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
610
|
+
this._signals.set(path, signal2(value));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
for (const [path, value] of paths) {
|
|
614
|
+
if (!this._signals.has(path)) {
|
|
615
|
+
this._signals.set(path, signal2(value));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
this._formErrors = signal2([]);
|
|
619
|
+
this.isSubmitting = signal2(false);
|
|
620
|
+
this.submitState = signal2("idle");
|
|
621
|
+
this.submitCount = signal2(0);
|
|
622
|
+
this.isDirty = signal2(false);
|
|
623
|
+
this.isValid = signal2(true);
|
|
624
|
+
this.values = createValuesProxy(this);
|
|
625
|
+
this._validationEngine = new ValidationEngine(this);
|
|
626
|
+
if (config.validateOnMount) {
|
|
627
|
+
Promise.resolve().then(() => {
|
|
628
|
+
this._validationEngine.validateAll().catch(() => {});
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
getSignal(path) {
|
|
633
|
+
let sig = this._signals.get(path);
|
|
634
|
+
if (!sig) {
|
|
635
|
+
let value = getPath(this._baseline, path);
|
|
636
|
+
if (value === undefined) {
|
|
637
|
+
const parts = path.split(".");
|
|
638
|
+
for (let i = parts.length - 1;i >= 1; i--) {
|
|
639
|
+
const parentPath = parts.slice(0, i).join(".");
|
|
640
|
+
const parentSig = this._signals.get(parentPath);
|
|
641
|
+
if (parentSig) {
|
|
642
|
+
const parentVal = parentSig.get();
|
|
643
|
+
if (parentVal != null && typeof parentVal === "object") {
|
|
644
|
+
const rest = parts.slice(i);
|
|
645
|
+
let current = parentVal;
|
|
646
|
+
for (const part of rest) {
|
|
647
|
+
if (current == null || typeof current !== "object") {
|
|
648
|
+
current = undefined;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
current = current[part];
|
|
652
|
+
}
|
|
653
|
+
if (current !== undefined)
|
|
654
|
+
value = current;
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
sig = signal2(value);
|
|
661
|
+
this._signals.set(path, sig);
|
|
662
|
+
}
|
|
663
|
+
return sig;
|
|
664
|
+
}
|
|
665
|
+
getErrors(path) {
|
|
666
|
+
let sig = this._errorSignals.get(path);
|
|
667
|
+
if (!sig) {
|
|
668
|
+
sig = signal2([]);
|
|
669
|
+
this._errorSignals.set(path, sig);
|
|
670
|
+
}
|
|
671
|
+
return sig;
|
|
672
|
+
}
|
|
673
|
+
getFormErrors() {
|
|
674
|
+
return this._formErrors;
|
|
675
|
+
}
|
|
676
|
+
getValue(path) {
|
|
677
|
+
return this.getSignal(path).get();
|
|
678
|
+
}
|
|
679
|
+
setValue(path, value) {
|
|
680
|
+
batch(() => {
|
|
681
|
+
const sig = this.getSignal(path);
|
|
682
|
+
const oldValue = sig.get();
|
|
683
|
+
if (oldValue === value)
|
|
684
|
+
return;
|
|
685
|
+
sig.set(value);
|
|
686
|
+
if (value !== null && typeof value === "object") {
|
|
687
|
+
this._syncChildSignals(path, value);
|
|
688
|
+
}
|
|
689
|
+
this._updateParentSignals(path, value);
|
|
690
|
+
const baselineValue = getPath(this._baseline, path);
|
|
691
|
+
if (deepEqual(value, baselineValue)) {
|
|
692
|
+
this._dirtySet.delete(path);
|
|
693
|
+
} else {
|
|
694
|
+
this._dirtySet.add(path);
|
|
695
|
+
}
|
|
696
|
+
this.isDirty.set(this._dirtySet.size > 0);
|
|
697
|
+
this._validationEngine.onFieldChange(path);
|
|
698
|
+
this._notifyWatchers(path, value);
|
|
699
|
+
this._notifySubscribers();
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
setValues(partial) {
|
|
703
|
+
batch(() => {
|
|
704
|
+
const paths = flattenToPaths(partial);
|
|
705
|
+
for (const [path, value] of paths) {
|
|
706
|
+
if (typeof value !== "object" || value === null) {
|
|
707
|
+
this.setValue(path, value);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
getValues() {
|
|
713
|
+
return this._getCurrentValues();
|
|
714
|
+
}
|
|
715
|
+
_syncChildSignals(path, value) {
|
|
716
|
+
if (Array.isArray(value)) {
|
|
717
|
+
for (let i = 0;i < value.length; i++) {
|
|
718
|
+
const childPath = `${path}.${i}`;
|
|
719
|
+
const childSig = this._signals.get(childPath);
|
|
720
|
+
if (childSig) {
|
|
721
|
+
childSig.set(value[i]);
|
|
722
|
+
}
|
|
723
|
+
if (value[i] !== null && typeof value[i] === "object") {
|
|
724
|
+
this._syncChildSignals(childPath, value[i]);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const prefix = path + ".";
|
|
728
|
+
for (const key of [...this._signals.keys()]) {
|
|
729
|
+
if (key.startsWith(prefix)) {
|
|
730
|
+
const rest = key.slice(prefix.length);
|
|
731
|
+
const dotIdx = rest.indexOf(".");
|
|
732
|
+
const segment = dotIdx === -1 ? rest : rest.slice(0, dotIdx);
|
|
733
|
+
const idx = parseInt(segment, 10);
|
|
734
|
+
if (!isNaN(idx) && idx >= value.length) {
|
|
735
|
+
this._signals.delete(key);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
} else if (typeof value === "object" && value !== null) {
|
|
740
|
+
for (const key of Object.keys(value)) {
|
|
741
|
+
const childPath = `${path}.${key}`;
|
|
742
|
+
const childSig = this._signals.get(childPath);
|
|
743
|
+
if (childSig) {
|
|
744
|
+
childSig.set(value[key]);
|
|
745
|
+
}
|
|
746
|
+
if (value[key] !== null && typeof value[key] === "object") {
|
|
747
|
+
this._syncChildSignals(childPath, value[key]);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
_updateParentSignals(path, _childValue) {
|
|
753
|
+
const parts = path.split(".");
|
|
754
|
+
for (let i = parts.length - 1;i > 0; i--) {
|
|
755
|
+
const parentPath = parts.slice(0, i).join(".");
|
|
756
|
+
const parentSig = this._signals.get(parentPath);
|
|
757
|
+
if (parentSig) {
|
|
758
|
+
const newValue = this._reconstructValue(parentPath);
|
|
759
|
+
parentSig.set(newValue);
|
|
760
|
+
this._notifyWatchers(parentPath, newValue);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
_reconstructValue(path) {
|
|
765
|
+
const baselineVal = getPath(this._baseline, path);
|
|
766
|
+
const currentSigVal = this._signals.get(path)?.get();
|
|
767
|
+
if (baselineVal === null || typeof baselineVal !== "object") {
|
|
768
|
+
return this.getSignal(path).get();
|
|
769
|
+
}
|
|
770
|
+
const prefix = path + ".";
|
|
771
|
+
const shapeSource = currentSigVal !== null && typeof currentSigVal === "object" ? currentSigVal : baselineVal;
|
|
772
|
+
if (Array.isArray(shapeSource) || Array.isArray(baselineVal)) {
|
|
773
|
+
const sourceArray = Array.isArray(shapeSource) ? shapeSource : baselineVal;
|
|
774
|
+
let maxLen = sourceArray.length;
|
|
775
|
+
for (const key of this._signals.keys()) {
|
|
776
|
+
if (key.startsWith(prefix)) {
|
|
777
|
+
const rest = key.slice(prefix.length);
|
|
778
|
+
const dotIdx = rest.indexOf(".");
|
|
779
|
+
const segment = dotIdx === -1 ? rest : rest.slice(0, dotIdx);
|
|
780
|
+
const idx = parseInt(segment, 10);
|
|
781
|
+
if (!isNaN(idx) && idx >= maxLen) {
|
|
782
|
+
maxLen = idx + 1;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const result2 = [];
|
|
787
|
+
for (let i = 0;i < maxLen; i++) {
|
|
788
|
+
const childPath = `${path}.${i}`;
|
|
789
|
+
const childSig = this._signals.get(childPath);
|
|
790
|
+
if (childSig) {
|
|
791
|
+
result2[i] = childSig.get();
|
|
792
|
+
} else {
|
|
793
|
+
const fromCurrent = Array.isArray(currentSigVal) ? currentSigVal[i] : undefined;
|
|
794
|
+
result2[i] = fromCurrent ?? baselineVal?.[i] ?? undefined;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return result2;
|
|
798
|
+
}
|
|
799
|
+
const result = {};
|
|
800
|
+
const keys = new Set(Object.keys(baselineVal));
|
|
801
|
+
for (const key of this._signals.keys()) {
|
|
802
|
+
if (key.startsWith(prefix)) {
|
|
803
|
+
const rest = key.slice(prefix.length);
|
|
804
|
+
const dotIdx = rest.indexOf(".");
|
|
805
|
+
const segment = dotIdx === -1 ? rest : rest.slice(0, dotIdx);
|
|
806
|
+
keys.add(segment);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
for (const key of keys) {
|
|
810
|
+
const childPath = `${path}.${key}`;
|
|
811
|
+
const childSig = this._signals.get(childPath);
|
|
812
|
+
result[key] = childSig ? childSig.get() : baselineVal[key];
|
|
813
|
+
}
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
setErrors(path, errors) {
|
|
817
|
+
this.getErrors(path).set(errors);
|
|
818
|
+
this._updateIsValid();
|
|
819
|
+
this._notifySubscribers();
|
|
820
|
+
}
|
|
821
|
+
clearErrors(path) {
|
|
822
|
+
if (path) {
|
|
823
|
+
const sig = this._errorSignals.get(path);
|
|
824
|
+
if (sig)
|
|
825
|
+
sig.set([]);
|
|
826
|
+
} else {
|
|
827
|
+
for (const sig of this._errorSignals.values()) {
|
|
828
|
+
sig.set([]);
|
|
829
|
+
}
|
|
830
|
+
this._formErrors.set([]);
|
|
831
|
+
}
|
|
832
|
+
this._updateIsValid();
|
|
833
|
+
this._notifySubscribers();
|
|
834
|
+
}
|
|
835
|
+
setFormErrors(errors) {
|
|
836
|
+
this._formErrors.set(errors);
|
|
837
|
+
this._updateIsValid();
|
|
838
|
+
this._notifySubscribers();
|
|
839
|
+
}
|
|
840
|
+
_updateIsValid() {
|
|
841
|
+
let valid = this._formErrors.get().length === 0;
|
|
842
|
+
if (valid) {
|
|
843
|
+
for (const sig of this._errorSignals.values()) {
|
|
844
|
+
if (sig.get().length > 0) {
|
|
845
|
+
valid = false;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
this.isValid.set(valid);
|
|
851
|
+
}
|
|
852
|
+
getTouched(path) {
|
|
853
|
+
return this._touchedSet.has(path);
|
|
854
|
+
}
|
|
855
|
+
setTouched(path, touched = true) {
|
|
856
|
+
if (touched) {
|
|
857
|
+
this._touchedSet.add(path);
|
|
858
|
+
} else {
|
|
859
|
+
this._touchedSet.delete(path);
|
|
860
|
+
}
|
|
861
|
+
this._notifySubscribers();
|
|
862
|
+
}
|
|
863
|
+
triggerBlurValidation(path) {
|
|
864
|
+
this._validationEngine.onFieldBlur(path);
|
|
865
|
+
}
|
|
866
|
+
getDirty(path) {
|
|
867
|
+
return this._dirtySet.has(path);
|
|
868
|
+
}
|
|
869
|
+
register(path, options) {
|
|
870
|
+
if (options) {
|
|
871
|
+
this._fieldOptions.set(path, options);
|
|
872
|
+
if (options.validate) {
|
|
873
|
+
this._validationEngine.registerFieldValidators(path, Array.isArray(options.validate) ? options.validate : [options.validate], options.validateOn);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const value = this.getValue(path);
|
|
877
|
+
const errorsSig = this.getErrors(path);
|
|
878
|
+
const inputProps = {
|
|
879
|
+
name: path,
|
|
880
|
+
value,
|
|
881
|
+
onChange: (e) => {
|
|
882
|
+
let newValue;
|
|
883
|
+
if (options?.parse) {
|
|
884
|
+
newValue = options.parse(e);
|
|
885
|
+
} else if (e && typeof e === "object" && "target" in e) {
|
|
886
|
+
const target = e.target;
|
|
887
|
+
newValue = target.type === "checkbox" ? target.checked : target.value;
|
|
888
|
+
} else {
|
|
889
|
+
newValue = e;
|
|
890
|
+
}
|
|
891
|
+
if (options?.transform) {
|
|
892
|
+
newValue = options.transform(newValue);
|
|
893
|
+
}
|
|
894
|
+
this.setValue(path, newValue);
|
|
895
|
+
},
|
|
896
|
+
onBlur: () => {
|
|
897
|
+
this.setTouched(path);
|
|
898
|
+
this._validationEngine.onFieldBlur(path);
|
|
899
|
+
},
|
|
900
|
+
ref: (el) => {
|
|
901
|
+
this._fieldRefs.set(path, el);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
const errors = errorsSig.get();
|
|
905
|
+
if (errors.length > 0) {
|
|
906
|
+
inputProps["aria-invalid"] = true;
|
|
907
|
+
inputProps["aria-describedby"] = `${path}-error`;
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
inputProps,
|
|
911
|
+
state: {
|
|
912
|
+
value,
|
|
913
|
+
errors: errorsSig.get(),
|
|
914
|
+
touched: this.getTouched(path),
|
|
915
|
+
dirty: this.getDirty(path),
|
|
916
|
+
validating: this.getFieldValidating(path).get()
|
|
917
|
+
},
|
|
918
|
+
setValue: (v) => this.setValue(path, v),
|
|
919
|
+
setError: (errs) => this.setErrors(path, errs),
|
|
920
|
+
clearErrors: () => this.clearErrors(path),
|
|
921
|
+
setTouched: (t) => this.setTouched(path, t),
|
|
922
|
+
reset: () => {
|
|
923
|
+
const baseline = getPath(this._baseline, path);
|
|
924
|
+
this.setValue(path, baseline);
|
|
925
|
+
this.clearErrors(path);
|
|
926
|
+
this.setTouched(path, false);
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
unregister(path) {
|
|
931
|
+
this._fieldOptions.delete(path);
|
|
932
|
+
this._fieldRefs.delete(path);
|
|
933
|
+
this._validationEngine.unregisterField(path);
|
|
934
|
+
}
|
|
935
|
+
async handleSubmit(e) {
|
|
936
|
+
if (e)
|
|
937
|
+
e.preventDefault?.();
|
|
938
|
+
if (!this.config.onSubmit) {
|
|
939
|
+
await this._validationEngine.validateAll();
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
await this.submitWith(this.config.onSubmit);
|
|
943
|
+
}
|
|
944
|
+
async submitWith(handler, _submitId) {
|
|
945
|
+
if (this.isSubmitting.get()) {
|
|
946
|
+
this._submitAbort?.abort();
|
|
947
|
+
}
|
|
948
|
+
const generation = ++this._submitGeneration;
|
|
949
|
+
const abortController = new AbortController;
|
|
950
|
+
this._submitAbort = abortController;
|
|
951
|
+
batch(() => {
|
|
952
|
+
this.isSubmitting.set(true);
|
|
953
|
+
this.submitState.set("submitting");
|
|
954
|
+
for (const path of this._fieldOptions.keys()) {
|
|
955
|
+
this._touchedSet.add(path);
|
|
956
|
+
}
|
|
957
|
+
this._notifySubscribers();
|
|
958
|
+
});
|
|
959
|
+
try {
|
|
960
|
+
const validationResult = await this._validationEngine.validateAll();
|
|
961
|
+
if (generation !== this._submitGeneration)
|
|
962
|
+
return;
|
|
963
|
+
if (!validationResult.success) {
|
|
964
|
+
batch(() => {
|
|
965
|
+
if (validationResult.errors) {
|
|
966
|
+
for (const [path, errors] of Object.entries(validationResult.errors)) {
|
|
967
|
+
this.setErrors(path, errors);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
this.isSubmitting.set(false);
|
|
971
|
+
this.submitState.set("error");
|
|
972
|
+
});
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const values = this._getCurrentValues();
|
|
976
|
+
const formData = this._buildFormData(values);
|
|
977
|
+
await handler(values, {
|
|
978
|
+
values,
|
|
979
|
+
formData,
|
|
980
|
+
signal: abortController.signal
|
|
981
|
+
});
|
|
982
|
+
if (generation !== this._submitGeneration)
|
|
983
|
+
return;
|
|
984
|
+
batch(() => {
|
|
985
|
+
this.isSubmitting.set(false);
|
|
986
|
+
this.submitState.set("success");
|
|
987
|
+
this.submitCount.update((c) => c + 1);
|
|
988
|
+
});
|
|
989
|
+
if (this.config.resetOnSubmit) {
|
|
990
|
+
this.reset();
|
|
991
|
+
}
|
|
992
|
+
} catch (error) {
|
|
993
|
+
if (generation === this._submitGeneration && !abortController.signal.aborted) {
|
|
994
|
+
batch(() => {
|
|
995
|
+
this.isSubmitting.set(false);
|
|
996
|
+
this.submitState.set("error");
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async validate() {
|
|
1003
|
+
const result = await this._validationEngine.validateAll();
|
|
1004
|
+
return result.success;
|
|
1005
|
+
}
|
|
1006
|
+
reset() {
|
|
1007
|
+
this.resetTo(deepClone(this.config.defaultValues));
|
|
1008
|
+
}
|
|
1009
|
+
resetTo(values) {
|
|
1010
|
+
batch(() => {
|
|
1011
|
+
this._baseline = deepClone(values);
|
|
1012
|
+
const newPaths = flattenToPaths(values);
|
|
1013
|
+
const newPathSet = new Set(newPaths.keys());
|
|
1014
|
+
for (const path of this._signals.keys()) {
|
|
1015
|
+
if (!newPathSet.has(path)) {
|
|
1016
|
+
this._signals.delete(path);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
for (const path of this._errorSignals.keys()) {
|
|
1020
|
+
if (!newPathSet.has(path)) {
|
|
1021
|
+
this._errorSignals.delete(path);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
for (const [path, value] of newPaths) {
|
|
1025
|
+
const sig = this._signals.get(path);
|
|
1026
|
+
if (sig) {
|
|
1027
|
+
sig.set(value);
|
|
1028
|
+
} else {
|
|
1029
|
+
this._signals.set(path, signal2(value));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
this._touchedSet.clear();
|
|
1033
|
+
this._dirtySet.clear();
|
|
1034
|
+
this.isDirty.set(false);
|
|
1035
|
+
this.clearErrors();
|
|
1036
|
+
this.submitState.set("idle");
|
|
1037
|
+
this._notifySubscribers();
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
setBaseline(values) {
|
|
1041
|
+
batch(() => {
|
|
1042
|
+
this._baseline = deepClone(values);
|
|
1043
|
+
for (const path of this._signals.keys()) {
|
|
1044
|
+
const current = this.getValue(path);
|
|
1045
|
+
const base = getPath(this._baseline, path);
|
|
1046
|
+
if (deepEqual(current, base)) {
|
|
1047
|
+
this._dirtySet.delete(path);
|
|
1048
|
+
} else {
|
|
1049
|
+
this._dirtySet.add(path);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
this.isDirty.set(this._dirtySet.size > 0);
|
|
1053
|
+
this._notifySubscribers();
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
getChanges() {
|
|
1057
|
+
let changes = {};
|
|
1058
|
+
for (const path of this._dirtySet) {
|
|
1059
|
+
const value = this.getValue(path);
|
|
1060
|
+
changes = setPath(changes, path, value);
|
|
1061
|
+
}
|
|
1062
|
+
return changes;
|
|
1063
|
+
}
|
|
1064
|
+
watch(path, callback) {
|
|
1065
|
+
if (!this._watchers.has(path)) {
|
|
1066
|
+
this._watchers.set(path, new Set);
|
|
1067
|
+
}
|
|
1068
|
+
this._watchers.get(path).add(callback);
|
|
1069
|
+
return () => {
|
|
1070
|
+
const set = this._watchers.get(path);
|
|
1071
|
+
if (set) {
|
|
1072
|
+
set.delete(callback);
|
|
1073
|
+
if (set.size === 0)
|
|
1074
|
+
this._watchers.delete(path);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
watchFields(paths, callback) {
|
|
1079
|
+
const unsubs = paths.map((p) => this.watch(p, callback));
|
|
1080
|
+
return () => unsubs.forEach((fn) => fn());
|
|
1081
|
+
}
|
|
1082
|
+
subscribe(callback) {
|
|
1083
|
+
this._subscribers.add(callback);
|
|
1084
|
+
return () => {
|
|
1085
|
+
this._subscribers.delete(callback);
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
toJSON() {
|
|
1089
|
+
return this._getCurrentValues();
|
|
1090
|
+
}
|
|
1091
|
+
toFormData() {
|
|
1092
|
+
return this._buildFormData(this._getCurrentValues());
|
|
1093
|
+
}
|
|
1094
|
+
getFieldRef(path) {
|
|
1095
|
+
return this._fieldRefs.get(path) ?? null;
|
|
1096
|
+
}
|
|
1097
|
+
setFieldRef(path, el) {
|
|
1098
|
+
this._fieldRefs.set(path, el);
|
|
1099
|
+
}
|
|
1100
|
+
getFieldOptions(path) {
|
|
1101
|
+
return this._fieldOptions.get(path);
|
|
1102
|
+
}
|
|
1103
|
+
getBaseline() {
|
|
1104
|
+
return deepClone(this._baseline);
|
|
1105
|
+
}
|
|
1106
|
+
getFieldValidating(path) {
|
|
1107
|
+
return this._validationEngine.getFieldValidatingSignal(path);
|
|
1108
|
+
}
|
|
1109
|
+
dispose() {
|
|
1110
|
+
this._validationEngine.dispose();
|
|
1111
|
+
this._subscribers.clear();
|
|
1112
|
+
this._watchers.clear();
|
|
1113
|
+
this._fieldRefs.clear();
|
|
1114
|
+
this._fieldOptions.clear();
|
|
1115
|
+
if (this._submitAbort) {
|
|
1116
|
+
this._submitAbort.abort();
|
|
1117
|
+
this._submitAbort = null;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
_getCurrentValues() {
|
|
1121
|
+
const result = deepClone(this._baseline);
|
|
1122
|
+
for (const [path, sig] of this._signals) {
|
|
1123
|
+
const value = sig.get();
|
|
1124
|
+
const parts = path.split(".");
|
|
1125
|
+
const baseValue = getPath(this._baseline, path);
|
|
1126
|
+
if (baseValue === null || typeof baseValue !== "object" || Array.isArray(baseValue)) {
|
|
1127
|
+
let current = result;
|
|
1128
|
+
for (let i = 0;i < parts.length - 1; i++) {
|
|
1129
|
+
const part = parts[i];
|
|
1130
|
+
const idx = parseInt(part, 10);
|
|
1131
|
+
if (!isNaN(idx)) {
|
|
1132
|
+
if (!current[idx])
|
|
1133
|
+
current[idx] = {};
|
|
1134
|
+
current = current[idx];
|
|
1135
|
+
} else {
|
|
1136
|
+
if (!current[part])
|
|
1137
|
+
current[part] = {};
|
|
1138
|
+
current = current[part];
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (current != null) {
|
|
1142
|
+
const lastPart = parts[parts.length - 1];
|
|
1143
|
+
const lastIdx = parseInt(lastPart, 10);
|
|
1144
|
+
if (!isNaN(lastIdx)) {
|
|
1145
|
+
current[lastIdx] = value;
|
|
1146
|
+
} else {
|
|
1147
|
+
current[lastPart] = value;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return result;
|
|
1153
|
+
}
|
|
1154
|
+
_buildFormData(values) {
|
|
1155
|
+
if (typeof FormData === "undefined") {
|
|
1156
|
+
throw new Error("FormData is not available in this environment. toFormData() cannot be used during SSR.");
|
|
1157
|
+
}
|
|
1158
|
+
const fd = new FormData;
|
|
1159
|
+
const flat = flattenToPaths(values);
|
|
1160
|
+
for (const [path, value] of flat) {
|
|
1161
|
+
if (value instanceof File) {
|
|
1162
|
+
fd.append(path, value);
|
|
1163
|
+
} else if (typeof value !== "object" || value === null) {
|
|
1164
|
+
fd.append(path, String(value ?? ""));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return fd;
|
|
1168
|
+
}
|
|
1169
|
+
_notifyWatchers(path, value) {
|
|
1170
|
+
const set = this._watchers.get(path);
|
|
1171
|
+
if (set) {
|
|
1172
|
+
for (const cb of [...set]) {
|
|
1173
|
+
cb(value, path);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
_notifySubscribers() {
|
|
1178
|
+
for (const cb of [...this._subscribers]) {
|
|
1179
|
+
cb();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function createFormStore(config) {
|
|
1184
|
+
return new FormStore(config);
|
|
1185
|
+
}
|
|
1186
|
+
// src/hooks.ts
|
|
1187
|
+
import { useRef, useCallback, useMemo, useEffect, useSyncExternalStore } from "react";
|
|
1188
|
+
import { useSignal } from "@ereo/state";
|
|
1189
|
+
function useForm(config) {
|
|
1190
|
+
const storeRef = useRef(null);
|
|
1191
|
+
if (!storeRef.current) {
|
|
1192
|
+
storeRef.current = new FormStore(config);
|
|
1193
|
+
}
|
|
1194
|
+
useEffect(() => {
|
|
1195
|
+
return () => {
|
|
1196
|
+
storeRef.current?.dispose();
|
|
1197
|
+
};
|
|
1198
|
+
}, []);
|
|
1199
|
+
return storeRef.current;
|
|
1200
|
+
}
|
|
1201
|
+
function useField(form, name, opts) {
|
|
1202
|
+
const optsRef = useRef(opts);
|
|
1203
|
+
const registered = useRef(false);
|
|
1204
|
+
if (!registered.current || optsRef.current !== opts) {
|
|
1205
|
+
optsRef.current = opts;
|
|
1206
|
+
if (opts) {
|
|
1207
|
+
form.register(name, opts);
|
|
1208
|
+
}
|
|
1209
|
+
registered.current = true;
|
|
1210
|
+
}
|
|
1211
|
+
useEffect(() => {
|
|
1212
|
+
return () => {
|
|
1213
|
+
form.unregister(name);
|
|
1214
|
+
};
|
|
1215
|
+
}, [form, name]);
|
|
1216
|
+
const valueSig = form.getSignal(name);
|
|
1217
|
+
const value = useSignal(valueSig);
|
|
1218
|
+
const errorsSig = form.getErrors(name);
|
|
1219
|
+
const errors = useSignal(errorsSig);
|
|
1220
|
+
const touched = useSyncExternalStore(useCallback((cb) => form.subscribe(cb), [form]), () => form.getTouched(name), () => form.getTouched(name));
|
|
1221
|
+
const dirty = useSyncExternalStore(useCallback((cb) => form.subscribe(cb), [form]), () => form.getDirty(name), () => form.getDirty(name));
|
|
1222
|
+
const validatingSig = form.getFieldValidating(name);
|
|
1223
|
+
const validating = useSignal(validatingSig);
|
|
1224
|
+
const setValue = useCallback((v) => form.setValue(name, v), [form, name]);
|
|
1225
|
+
const setError = useCallback((errs) => form.setErrors(name, errs), [form, name]);
|
|
1226
|
+
const clearErrors = useCallback(() => form.clearErrors(name), [form, name]);
|
|
1227
|
+
const setTouched = useCallback((t) => form.setTouched(name, t), [form, name]);
|
|
1228
|
+
const reset = useCallback(() => {
|
|
1229
|
+
const baseline = getPath(form.getBaseline(), name);
|
|
1230
|
+
form.setValue(name, baseline);
|
|
1231
|
+
form.clearErrors(name);
|
|
1232
|
+
form.setTouched(name, false);
|
|
1233
|
+
}, [form, name]);
|
|
1234
|
+
const onChange = useCallback((e) => {
|
|
1235
|
+
let newValue;
|
|
1236
|
+
if (opts?.parse) {
|
|
1237
|
+
newValue = opts.parse(e);
|
|
1238
|
+
} else if (e && typeof e === "object" && "target" in e) {
|
|
1239
|
+
const target = e.target;
|
|
1240
|
+
newValue = target.type === "checkbox" ? target.checked : target.value;
|
|
1241
|
+
} else {
|
|
1242
|
+
newValue = e;
|
|
1243
|
+
}
|
|
1244
|
+
if (opts?.transform) {
|
|
1245
|
+
newValue = opts.transform(newValue);
|
|
1246
|
+
}
|
|
1247
|
+
form.setValue(name, newValue);
|
|
1248
|
+
}, [form, name, opts]);
|
|
1249
|
+
const onBlur = useCallback(() => {
|
|
1250
|
+
form.setTouched(name);
|
|
1251
|
+
form.triggerBlurValidation(name);
|
|
1252
|
+
}, [form, name]);
|
|
1253
|
+
const refCallback = useCallback((el) => {
|
|
1254
|
+
if (form.setFieldRef) {
|
|
1255
|
+
form.setFieldRef(name, el);
|
|
1256
|
+
}
|
|
1257
|
+
}, [form, name]);
|
|
1258
|
+
const inputProps = useMemo(() => ({
|
|
1259
|
+
name,
|
|
1260
|
+
value,
|
|
1261
|
+
onChange,
|
|
1262
|
+
onBlur,
|
|
1263
|
+
ref: refCallback,
|
|
1264
|
+
...errors.length > 0 ? {
|
|
1265
|
+
"aria-invalid": true,
|
|
1266
|
+
"aria-describedby": `${name}-error`
|
|
1267
|
+
} : {}
|
|
1268
|
+
}), [name, value, onChange, onBlur, refCallback, errors]);
|
|
1269
|
+
return {
|
|
1270
|
+
inputProps,
|
|
1271
|
+
value,
|
|
1272
|
+
errors,
|
|
1273
|
+
touched,
|
|
1274
|
+
dirty,
|
|
1275
|
+
validating,
|
|
1276
|
+
setValue,
|
|
1277
|
+
setError,
|
|
1278
|
+
clearErrors,
|
|
1279
|
+
setTouched,
|
|
1280
|
+
reset
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function useFieldArray(form, name) {
|
|
1284
|
+
const idCounter = useRef(0);
|
|
1285
|
+
const idsRef = useRef([]);
|
|
1286
|
+
const arraySig = form.getSignal(name);
|
|
1287
|
+
const rawArray = useSignal(arraySig);
|
|
1288
|
+
const items = rawArray ?? [];
|
|
1289
|
+
if (idsRef.current.length < items.length) {
|
|
1290
|
+
for (let i = idsRef.current.length;i < items.length; i++) {
|
|
1291
|
+
idsRef.current.push(`${name}-${idCounter.current++}`);
|
|
1292
|
+
}
|
|
1293
|
+
} else if (idsRef.current.length > items.length) {
|
|
1294
|
+
idsRef.current.length = items.length;
|
|
1295
|
+
}
|
|
1296
|
+
const fields = useMemo(() => items.map((value, index) => ({
|
|
1297
|
+
id: idsRef.current[index] ?? `${name}-fallback-${index}`,
|
|
1298
|
+
value,
|
|
1299
|
+
index
|
|
1300
|
+
})), [items, name]);
|
|
1301
|
+
const append = useCallback((value) => {
|
|
1302
|
+
const current = form.getValue(name) ?? [];
|
|
1303
|
+
idsRef.current = [...idsRef.current, `${name}-${idCounter.current++}`];
|
|
1304
|
+
form.setValue(name, [...current, value]);
|
|
1305
|
+
}, [form, name]);
|
|
1306
|
+
const prepend = useCallback((value) => {
|
|
1307
|
+
const current = form.getValue(name) ?? [];
|
|
1308
|
+
idsRef.current = [`${name}-${idCounter.current++}`, ...idsRef.current];
|
|
1309
|
+
form.setValue(name, [value, ...current]);
|
|
1310
|
+
}, [form, name]);
|
|
1311
|
+
const insert = useCallback((index, value) => {
|
|
1312
|
+
const current = form.getValue(name) ?? [];
|
|
1313
|
+
const next = [...current];
|
|
1314
|
+
next.splice(index, 0, value);
|
|
1315
|
+
const ids = [...idsRef.current];
|
|
1316
|
+
ids.splice(index, 0, `${name}-${idCounter.current++}`);
|
|
1317
|
+
idsRef.current = ids;
|
|
1318
|
+
form.setValue(name, next);
|
|
1319
|
+
}, [form, name]);
|
|
1320
|
+
const remove = useCallback((index) => {
|
|
1321
|
+
const current = form.getValue(name) ?? [];
|
|
1322
|
+
const next = [...current];
|
|
1323
|
+
next.splice(index, 1);
|
|
1324
|
+
const ids = [...idsRef.current];
|
|
1325
|
+
ids.splice(index, 1);
|
|
1326
|
+
idsRef.current = ids;
|
|
1327
|
+
form.setValue(name, next);
|
|
1328
|
+
}, [form, name]);
|
|
1329
|
+
const swap = useCallback((indexA, indexB) => {
|
|
1330
|
+
const current = form.getValue(name) ?? [];
|
|
1331
|
+
const next = [...current];
|
|
1332
|
+
[next[indexA], next[indexB]] = [next[indexB], next[indexA]];
|
|
1333
|
+
const ids = [...idsRef.current];
|
|
1334
|
+
[ids[indexA], ids[indexB]] = [ids[indexB], ids[indexA]];
|
|
1335
|
+
idsRef.current = ids;
|
|
1336
|
+
form.setValue(name, next);
|
|
1337
|
+
}, [form, name]);
|
|
1338
|
+
const move = useCallback((from, to) => {
|
|
1339
|
+
const current = form.getValue(name) ?? [];
|
|
1340
|
+
const next = [...current];
|
|
1341
|
+
const [item] = next.splice(from, 1);
|
|
1342
|
+
next.splice(to, 0, item);
|
|
1343
|
+
const ids = [...idsRef.current];
|
|
1344
|
+
const [id] = ids.splice(from, 1);
|
|
1345
|
+
ids.splice(to, 0, id);
|
|
1346
|
+
idsRef.current = ids;
|
|
1347
|
+
form.setValue(name, next);
|
|
1348
|
+
}, [form, name]);
|
|
1349
|
+
const replace = useCallback((index, value) => {
|
|
1350
|
+
const current = form.getValue(name) ?? [];
|
|
1351
|
+
const next = [...current];
|
|
1352
|
+
next[index] = value;
|
|
1353
|
+
form.setValue(name, next);
|
|
1354
|
+
}, [form, name]);
|
|
1355
|
+
const replaceAll = useCallback((values) => {
|
|
1356
|
+
idsRef.current = values.map(() => `${name}-${idCounter.current++}`);
|
|
1357
|
+
form.setValue(name, values);
|
|
1358
|
+
}, [form, name]);
|
|
1359
|
+
const clone = useCallback((index) => {
|
|
1360
|
+
const current = form.getValue(name) ?? [];
|
|
1361
|
+
const next = [...current];
|
|
1362
|
+
const cloned = deepClone(current[index]);
|
|
1363
|
+
next.splice(index + 1, 0, cloned);
|
|
1364
|
+
const ids = [...idsRef.current];
|
|
1365
|
+
ids.splice(index + 1, 0, `${name}-${idCounter.current++}`);
|
|
1366
|
+
idsRef.current = ids;
|
|
1367
|
+
form.setValue(name, next);
|
|
1368
|
+
}, [form, name]);
|
|
1369
|
+
return {
|
|
1370
|
+
fields,
|
|
1371
|
+
append,
|
|
1372
|
+
prepend,
|
|
1373
|
+
insert,
|
|
1374
|
+
remove,
|
|
1375
|
+
swap,
|
|
1376
|
+
move,
|
|
1377
|
+
replace,
|
|
1378
|
+
replaceAll,
|
|
1379
|
+
clone
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function useFormStatus(form) {
|
|
1383
|
+
const isSubmitting = useSignal(form.isSubmitting);
|
|
1384
|
+
const submitState = useSignal(form.submitState);
|
|
1385
|
+
const isValid = useSignal(form.isValid);
|
|
1386
|
+
const isDirty = useSignal(form.isDirty);
|
|
1387
|
+
const submitCount = useSignal(form.submitCount);
|
|
1388
|
+
return { isSubmitting, submitState, isValid, isDirty, submitCount };
|
|
1389
|
+
}
|
|
1390
|
+
// src/context.ts
|
|
1391
|
+
import { createContext, useContext, createElement } from "react";
|
|
1392
|
+
var FormContext = createContext(null);
|
|
1393
|
+
function FormProvider({
|
|
1394
|
+
form,
|
|
1395
|
+
children
|
|
1396
|
+
}) {
|
|
1397
|
+
return createElement(FormContext.Provider, { value: form }, children);
|
|
1398
|
+
}
|
|
1399
|
+
function useFormContext() {
|
|
1400
|
+
return useContext(FormContext);
|
|
1401
|
+
}
|
|
1402
|
+
// src/components.ts
|
|
1403
|
+
import { createElement as createElement2 } from "react";
|
|
1404
|
+
|
|
1405
|
+
// src/a11y.ts
|
|
1406
|
+
var idCounter = 0;
|
|
1407
|
+
function generateA11yId(prefix = "ereo") {
|
|
1408
|
+
return `${prefix}-${++idCounter}`;
|
|
1409
|
+
}
|
|
1410
|
+
function getFieldA11y(name, state) {
|
|
1411
|
+
const attrs = {};
|
|
1412
|
+
if (state.errors.length > 0 && state.touched) {
|
|
1413
|
+
attrs["aria-invalid"] = true;
|
|
1414
|
+
attrs["aria-describedby"] = `${name}-error`;
|
|
1415
|
+
}
|
|
1416
|
+
return attrs;
|
|
1417
|
+
}
|
|
1418
|
+
function getErrorA11y(name) {
|
|
1419
|
+
return {
|
|
1420
|
+
id: `${name}-error`,
|
|
1421
|
+
role: "alert",
|
|
1422
|
+
"aria-live": "polite"
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function getLabelA11y(name, opts) {
|
|
1426
|
+
return {
|
|
1427
|
+
htmlFor: name,
|
|
1428
|
+
id: opts?.id ?? `${name}-label`
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function getDescriptionA11y(name) {
|
|
1432
|
+
return {
|
|
1433
|
+
id: `${name}-description`
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function getFieldsetA11y(name, _legend) {
|
|
1437
|
+
return {
|
|
1438
|
+
role: "group",
|
|
1439
|
+
"aria-labelledby": `${name}-legend`
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function getFieldWrapperA11y(name, state) {
|
|
1443
|
+
const attrs = {};
|
|
1444
|
+
attrs["data-field"] = name;
|
|
1445
|
+
if (state.errors.length > 0 && state.touched) {
|
|
1446
|
+
attrs["data-invalid"] = true;
|
|
1447
|
+
}
|
|
1448
|
+
return attrs;
|
|
1449
|
+
}
|
|
1450
|
+
function getFormA11y(id, opts) {
|
|
1451
|
+
const attrs = {
|
|
1452
|
+
id,
|
|
1453
|
+
role: "form"
|
|
1454
|
+
};
|
|
1455
|
+
if (opts?.isSubmitting) {
|
|
1456
|
+
attrs["aria-busy"] = true;
|
|
1457
|
+
}
|
|
1458
|
+
return attrs;
|
|
1459
|
+
}
|
|
1460
|
+
function getErrorSummaryA11y(formId) {
|
|
1461
|
+
return {
|
|
1462
|
+
role: "alert",
|
|
1463
|
+
"aria-labelledby": `${formId}-error-summary`
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function focusFirstError(form) {
|
|
1467
|
+
if (typeof document === "undefined")
|
|
1468
|
+
return;
|
|
1469
|
+
const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
1470
|
+
const formStore = form;
|
|
1471
|
+
if (formStore._fieldRefs) {
|
|
1472
|
+
for (const [path, el] of formStore._fieldRefs) {
|
|
1473
|
+
if (!el)
|
|
1474
|
+
continue;
|
|
1475
|
+
const errors = form.getErrors(path).get();
|
|
1476
|
+
if (errors.length > 0) {
|
|
1477
|
+
el.focus();
|
|
1478
|
+
el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const elements = document.querySelectorAll('[aria-invalid="true"]');
|
|
1484
|
+
const first = elements[0];
|
|
1485
|
+
if (first) {
|
|
1486
|
+
first.focus();
|
|
1487
|
+
first.scrollIntoView({ behavior: scrollBehavior, block: "center" });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function focusField(name) {
|
|
1491
|
+
if (typeof document === "undefined")
|
|
1492
|
+
return;
|
|
1493
|
+
const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
1494
|
+
const el = document.querySelector(`[name="${name}"]`);
|
|
1495
|
+
if (el) {
|
|
1496
|
+
el.focus();
|
|
1497
|
+
el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function trapFocus(container) {
|
|
1501
|
+
if (typeof document === "undefined")
|
|
1502
|
+
return () => {};
|
|
1503
|
+
const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
1504
|
+
const handleKeydown = (e) => {
|
|
1505
|
+
if (e.key !== "Tab")
|
|
1506
|
+
return;
|
|
1507
|
+
const focusable = container.querySelectorAll(focusableSelector);
|
|
1508
|
+
if (focusable.length === 0)
|
|
1509
|
+
return;
|
|
1510
|
+
const first = focusable[0];
|
|
1511
|
+
const last = focusable[focusable.length - 1];
|
|
1512
|
+
if (e.shiftKey) {
|
|
1513
|
+
if (document.activeElement === first) {
|
|
1514
|
+
e.preventDefault();
|
|
1515
|
+
last.focus();
|
|
1516
|
+
}
|
|
1517
|
+
} else {
|
|
1518
|
+
if (document.activeElement === last) {
|
|
1519
|
+
e.preventDefault();
|
|
1520
|
+
first.focus();
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
container.addEventListener("keydown", handleKeydown);
|
|
1525
|
+
return () => container.removeEventListener("keydown", handleKeydown);
|
|
1526
|
+
}
|
|
1527
|
+
var liveRegion = null;
|
|
1528
|
+
function getOrCreateLiveRegion() {
|
|
1529
|
+
if (typeof document === "undefined") {
|
|
1530
|
+
return { textContent: "" };
|
|
1531
|
+
}
|
|
1532
|
+
if (!liveRegion) {
|
|
1533
|
+
liveRegion = document.createElement("div");
|
|
1534
|
+
liveRegion.setAttribute("role", "status");
|
|
1535
|
+
liveRegion.setAttribute("aria-live", "polite");
|
|
1536
|
+
liveRegion.setAttribute("aria-atomic", "true");
|
|
1537
|
+
liveRegion.style.position = "absolute";
|
|
1538
|
+
liveRegion.style.width = "1px";
|
|
1539
|
+
liveRegion.style.height = "1px";
|
|
1540
|
+
liveRegion.style.padding = "0";
|
|
1541
|
+
liveRegion.style.margin = "-1px";
|
|
1542
|
+
liveRegion.style.overflow = "hidden";
|
|
1543
|
+
liveRegion.style.clip = "rect(0, 0, 0, 0)";
|
|
1544
|
+
liveRegion.style.whiteSpace = "nowrap";
|
|
1545
|
+
liveRegion.style.borderWidth = "0";
|
|
1546
|
+
document.body.appendChild(liveRegion);
|
|
1547
|
+
}
|
|
1548
|
+
return liveRegion;
|
|
1549
|
+
}
|
|
1550
|
+
function announce(message, priority = "polite") {
|
|
1551
|
+
const region = getOrCreateLiveRegion();
|
|
1552
|
+
region.setAttribute("aria-live", priority);
|
|
1553
|
+
region.textContent = "";
|
|
1554
|
+
requestAnimationFrame(() => {
|
|
1555
|
+
region.textContent = message;
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
function cleanupLiveRegion() {
|
|
1559
|
+
if (liveRegion && typeof document !== "undefined") {
|
|
1560
|
+
liveRegion.remove();
|
|
1561
|
+
liveRegion = null;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function announceErrors(errors, opts) {
|
|
1565
|
+
const errorEntries = Object.entries(errors).filter(([_, msgs]) => msgs.length > 0);
|
|
1566
|
+
if (errorEntries.length === 0)
|
|
1567
|
+
return;
|
|
1568
|
+
const prefix = opts?.prefix ?? "Form has errors:";
|
|
1569
|
+
const messages = errorEntries.map(([field, msgs]) => `${field}: ${msgs[0]}`).join(". ");
|
|
1570
|
+
announce(`${prefix} ${messages}`, "assertive");
|
|
1571
|
+
}
|
|
1572
|
+
function announceSubmitStatus(status, opts) {
|
|
1573
|
+
switch (status) {
|
|
1574
|
+
case "submitting":
|
|
1575
|
+
announce(opts?.submittingMessage ?? "Submitting form...", "polite");
|
|
1576
|
+
break;
|
|
1577
|
+
case "success":
|
|
1578
|
+
announce(opts?.successMessage ?? "Form submitted successfully.", "polite");
|
|
1579
|
+
break;
|
|
1580
|
+
case "error":
|
|
1581
|
+
announce(opts?.errorMessage ?? "Form submission failed. Please check for errors.", "assertive");
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
function prefersReducedMotion() {
|
|
1586
|
+
if (typeof window === "undefined")
|
|
1587
|
+
return false;
|
|
1588
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1589
|
+
}
|
|
1590
|
+
function isScreenReaderActive() {
|
|
1591
|
+
if (typeof window === "undefined")
|
|
1592
|
+
return false;
|
|
1593
|
+
return document.querySelector('[role="application"]') !== null || window.navigator.userAgent.includes("NVDA") || window.navigator.userAgent.includes("JAWS");
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// src/components.ts
|
|
1597
|
+
function inferInputType(name, explicitType) {
|
|
1598
|
+
if (explicitType)
|
|
1599
|
+
return explicitType;
|
|
1600
|
+
const lastSegment = name.split(".").pop().toLowerCase();
|
|
1601
|
+
if (lastSegment.includes("email"))
|
|
1602
|
+
return "email";
|
|
1603
|
+
if (lastSegment.includes("password"))
|
|
1604
|
+
return "password";
|
|
1605
|
+
if (lastSegment.includes("phone") || lastSegment.includes("tel"))
|
|
1606
|
+
return "tel";
|
|
1607
|
+
if (lastSegment.includes("url") || lastSegment.includes("website"))
|
|
1608
|
+
return "url";
|
|
1609
|
+
if (lastSegment.includes("date"))
|
|
1610
|
+
return "date";
|
|
1611
|
+
if (lastSegment.includes("time"))
|
|
1612
|
+
return "time";
|
|
1613
|
+
if (lastSegment.includes("number") || lastSegment.includes("age") || lastSegment.includes("quantity"))
|
|
1614
|
+
return "number";
|
|
1615
|
+
if (lastSegment.includes("search"))
|
|
1616
|
+
return "search";
|
|
1617
|
+
return "text";
|
|
1618
|
+
}
|
|
1619
|
+
function inferRequired(form, name, explicit) {
|
|
1620
|
+
if (explicit !== undefined)
|
|
1621
|
+
return explicit;
|
|
1622
|
+
const opts = form.getFieldOptions?.(name);
|
|
1623
|
+
if (!opts?.validate)
|
|
1624
|
+
return false;
|
|
1625
|
+
const validators = Array.isArray(opts.validate) ? opts.validate : [opts.validate];
|
|
1626
|
+
return validators.some((v) => v._isRequired);
|
|
1627
|
+
}
|
|
1628
|
+
function Field(props) {
|
|
1629
|
+
const contextForm = useFormContext();
|
|
1630
|
+
const form = props.form ?? contextForm;
|
|
1631
|
+
if (!form)
|
|
1632
|
+
throw new Error("Field requires a form prop or FormProvider");
|
|
1633
|
+
const field = useField(form, props.name);
|
|
1634
|
+
const inputType = inferInputType(props.name, props.type);
|
|
1635
|
+
const isRequired = inferRequired(form, props.name, props.required);
|
|
1636
|
+
if (props.children) {
|
|
1637
|
+
return props.children(field);
|
|
1638
|
+
}
|
|
1639
|
+
const children = [];
|
|
1640
|
+
if (props.label) {
|
|
1641
|
+
children.push(createElement2("label", { key: "label", ...getLabelA11y(props.name) }, props.label, isRequired ? createElement2("span", { key: "req", "aria-hidden": true }, " *") : null));
|
|
1642
|
+
}
|
|
1643
|
+
children.push(createElement2("input", {
|
|
1644
|
+
key: "input",
|
|
1645
|
+
...field.inputProps,
|
|
1646
|
+
type: inputType,
|
|
1647
|
+
id: props.name,
|
|
1648
|
+
required: isRequired,
|
|
1649
|
+
disabled: props.disabled,
|
|
1650
|
+
placeholder: props.placeholder,
|
|
1651
|
+
className: props.className,
|
|
1652
|
+
"aria-required": isRequired || undefined
|
|
1653
|
+
}));
|
|
1654
|
+
if (field.errors.length > 0 && field.touched) {
|
|
1655
|
+
children.push(createElement2("div", { key: "error", ...getErrorA11y(props.name) }, field.errors[0]));
|
|
1656
|
+
}
|
|
1657
|
+
return createElement2("div", { "data-field": props.name }, ...children);
|
|
1658
|
+
}
|
|
1659
|
+
function TextareaField(props) {
|
|
1660
|
+
const contextForm = useFormContext();
|
|
1661
|
+
const form = props.form ?? contextForm;
|
|
1662
|
+
if (!form)
|
|
1663
|
+
throw new Error("TextareaField requires a form prop or FormProvider");
|
|
1664
|
+
const field = useField(form, props.name);
|
|
1665
|
+
const isRequired = inferRequired(form, props.name, props.required);
|
|
1666
|
+
if (props.children) {
|
|
1667
|
+
return props.children(field);
|
|
1668
|
+
}
|
|
1669
|
+
const children = [];
|
|
1670
|
+
if (props.label) {
|
|
1671
|
+
children.push(createElement2("label", { key: "label", ...getLabelA11y(props.name) }, props.label));
|
|
1672
|
+
}
|
|
1673
|
+
children.push(createElement2("textarea", {
|
|
1674
|
+
key: "textarea",
|
|
1675
|
+
...field.inputProps,
|
|
1676
|
+
id: props.name,
|
|
1677
|
+
rows: props.rows,
|
|
1678
|
+
cols: props.cols,
|
|
1679
|
+
maxLength: props.maxLength,
|
|
1680
|
+
required: isRequired,
|
|
1681
|
+
disabled: props.disabled,
|
|
1682
|
+
placeholder: props.placeholder,
|
|
1683
|
+
className: props.className,
|
|
1684
|
+
"aria-required": isRequired || undefined
|
|
1685
|
+
}));
|
|
1686
|
+
if (field.errors.length > 0 && field.touched) {
|
|
1687
|
+
children.push(createElement2("div", { key: "error", ...getErrorA11y(props.name) }, field.errors[0]));
|
|
1688
|
+
}
|
|
1689
|
+
return createElement2("div", { "data-field": props.name }, ...children);
|
|
1690
|
+
}
|
|
1691
|
+
function SelectField(props) {
|
|
1692
|
+
const contextForm = useFormContext();
|
|
1693
|
+
const form = props.form ?? contextForm;
|
|
1694
|
+
if (!form)
|
|
1695
|
+
throw new Error("SelectField requires a form prop or FormProvider");
|
|
1696
|
+
const field = useField(form, props.name);
|
|
1697
|
+
const isRequired = inferRequired(form, props.name, props.required);
|
|
1698
|
+
if (props.children) {
|
|
1699
|
+
return props.children(field);
|
|
1700
|
+
}
|
|
1701
|
+
const children = [];
|
|
1702
|
+
if (props.label) {
|
|
1703
|
+
children.push(createElement2("label", { key: "label", ...getLabelA11y(props.name) }, props.label));
|
|
1704
|
+
}
|
|
1705
|
+
const options = props.options.map((opt, i) => createElement2("option", { key: opt.value ?? i, value: opt.value, disabled: opt.disabled }, opt.label));
|
|
1706
|
+
children.push(createElement2("select", {
|
|
1707
|
+
key: "select",
|
|
1708
|
+
...field.inputProps,
|
|
1709
|
+
id: props.name,
|
|
1710
|
+
multiple: props.multiple,
|
|
1711
|
+
required: isRequired,
|
|
1712
|
+
disabled: props.disabled,
|
|
1713
|
+
className: props.className,
|
|
1714
|
+
"aria-required": isRequired || undefined
|
|
1715
|
+
}, ...options));
|
|
1716
|
+
if (field.errors.length > 0 && field.touched) {
|
|
1717
|
+
children.push(createElement2("div", { key: "error", ...getErrorA11y(props.name) }, field.errors[0]));
|
|
1718
|
+
}
|
|
1719
|
+
return createElement2("div", { "data-field": props.name }, ...children);
|
|
1720
|
+
}
|
|
1721
|
+
function FieldArrayComponent(props) {
|
|
1722
|
+
const contextForm = useFormContext();
|
|
1723
|
+
const form = props.form ?? contextForm;
|
|
1724
|
+
if (!form)
|
|
1725
|
+
throw new Error("FieldArray requires a form prop or FormProvider");
|
|
1726
|
+
const helpers = useFieldArray(form, props.name);
|
|
1727
|
+
return props.children(helpers);
|
|
1728
|
+
}
|
|
1729
|
+
// src/validators.ts
|
|
1730
|
+
function required(msg = "This field is required") {
|
|
1731
|
+
const fn = (value) => {
|
|
1732
|
+
if (value === null || value === undefined || value === "")
|
|
1733
|
+
return msg;
|
|
1734
|
+
if (Array.isArray(value) && value.length === 0)
|
|
1735
|
+
return msg;
|
|
1736
|
+
return;
|
|
1737
|
+
};
|
|
1738
|
+
fn._isRequired = true;
|
|
1739
|
+
return fn;
|
|
1740
|
+
}
|
|
1741
|
+
function email(msg = "Invalid email address") {
|
|
1742
|
+
return (value) => {
|
|
1743
|
+
if (!value)
|
|
1744
|
+
return;
|
|
1745
|
+
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1746
|
+
return re.test(String(value)) ? undefined : msg;
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function url(msg = "Invalid URL") {
|
|
1750
|
+
return (value) => {
|
|
1751
|
+
if (!value)
|
|
1752
|
+
return;
|
|
1753
|
+
try {
|
|
1754
|
+
new URL(String(value));
|
|
1755
|
+
return;
|
|
1756
|
+
} catch {
|
|
1757
|
+
return msg;
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function date(msg = "Invalid date") {
|
|
1762
|
+
return (value) => {
|
|
1763
|
+
if (!value)
|
|
1764
|
+
return;
|
|
1765
|
+
const d = new Date(String(value));
|
|
1766
|
+
return isNaN(d.getTime()) ? msg : undefined;
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
function phone(msg = "Invalid phone number") {
|
|
1770
|
+
return (value) => {
|
|
1771
|
+
if (!value)
|
|
1772
|
+
return;
|
|
1773
|
+
const str = String(value);
|
|
1774
|
+
const re = /^\+?[\d\s\-().]{7,}$/;
|
|
1775
|
+
const digitCount = (str.match(/\d/g) || []).length;
|
|
1776
|
+
return re.test(str) && digitCount >= 7 ? undefined : msg;
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
function minLength(n, msg) {
|
|
1780
|
+
return (value) => {
|
|
1781
|
+
if (!value)
|
|
1782
|
+
return;
|
|
1783
|
+
return String(value).length >= n ? undefined : msg ?? `Must be at least ${n} characters`;
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function maxLength(n, msg) {
|
|
1787
|
+
return (value) => {
|
|
1788
|
+
if (!value)
|
|
1789
|
+
return;
|
|
1790
|
+
return String(value).length <= n ? undefined : msg ?? `Must be at most ${n} characters`;
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
function min(n, msg) {
|
|
1794
|
+
return (value) => {
|
|
1795
|
+
if (value === null || value === undefined || value === "")
|
|
1796
|
+
return;
|
|
1797
|
+
return Number(value) >= n ? undefined : msg ?? `Must be at least ${n}`;
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
function max(n, msg) {
|
|
1801
|
+
return (value) => {
|
|
1802
|
+
if (value === null || value === undefined || value === "")
|
|
1803
|
+
return;
|
|
1804
|
+
return Number(value) <= n ? undefined : msg ?? `Must be at most ${n}`;
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function pattern(regex, msg = "Invalid format") {
|
|
1808
|
+
return (value) => {
|
|
1809
|
+
if (!value)
|
|
1810
|
+
return;
|
|
1811
|
+
return regex.test(String(value)) ? undefined : msg;
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
function number(msg = "Must be a number") {
|
|
1815
|
+
return (value) => {
|
|
1816
|
+
if (value === null || value === undefined || value === "")
|
|
1817
|
+
return;
|
|
1818
|
+
return isNaN(Number(value)) ? msg : undefined;
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function integer(msg = "Must be an integer") {
|
|
1822
|
+
return (value) => {
|
|
1823
|
+
if (value === null || value === undefined || value === "")
|
|
1824
|
+
return;
|
|
1825
|
+
return Number.isInteger(Number(value)) ? undefined : msg;
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function positive(msg = "Must be a positive number") {
|
|
1829
|
+
return (value) => {
|
|
1830
|
+
if (value === null || value === undefined || value === "")
|
|
1831
|
+
return;
|
|
1832
|
+
return Number(value) > 0 ? undefined : msg;
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function custom(fn, msg) {
|
|
1836
|
+
return (value) => {
|
|
1837
|
+
const result = fn(value);
|
|
1838
|
+
if (result !== undefined)
|
|
1839
|
+
return result;
|
|
1840
|
+
return;
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function async(fn, opts) {
|
|
1844
|
+
const validator = (value) => fn(value);
|
|
1845
|
+
validator._isAsync = true;
|
|
1846
|
+
if (opts?.debounce)
|
|
1847
|
+
validator._debounce = opts.debounce;
|
|
1848
|
+
return validator;
|
|
1849
|
+
}
|
|
1850
|
+
function matches(otherField, msg) {
|
|
1851
|
+
const validator = (value, context) => {
|
|
1852
|
+
if (!context)
|
|
1853
|
+
return;
|
|
1854
|
+
const other = context.getValue(otherField);
|
|
1855
|
+
return value === other ? undefined : msg ?? `Must match ${otherField}`;
|
|
1856
|
+
};
|
|
1857
|
+
validator._crossField = true;
|
|
1858
|
+
return validator;
|
|
1859
|
+
}
|
|
1860
|
+
function oneOf(values, msg) {
|
|
1861
|
+
return (value) => {
|
|
1862
|
+
if (value === null || value === undefined || value === "")
|
|
1863
|
+
return;
|
|
1864
|
+
return values.includes(value) ? undefined : msg ?? `Must be one of: ${values.join(", ")}`;
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function notOneOf(values, msg) {
|
|
1868
|
+
return (value) => {
|
|
1869
|
+
if (value === null || value === undefined || value === "")
|
|
1870
|
+
return;
|
|
1871
|
+
return !values.includes(value) ? undefined : msg ?? `Must not be one of: ${values.join(", ")}`;
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
function fileSize(maxBytes, msg) {
|
|
1875
|
+
return (value) => {
|
|
1876
|
+
if (!value)
|
|
1877
|
+
return;
|
|
1878
|
+
const file = value;
|
|
1879
|
+
if (!file.size)
|
|
1880
|
+
return;
|
|
1881
|
+
return file.size <= maxBytes ? undefined : msg ?? `File must be less than ${Math.round(maxBytes / 1024)}KB`;
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
function fileType(types, msg) {
|
|
1885
|
+
return (value) => {
|
|
1886
|
+
if (!value)
|
|
1887
|
+
return;
|
|
1888
|
+
const file = value;
|
|
1889
|
+
if (!file.type)
|
|
1890
|
+
return;
|
|
1891
|
+
return types.some((t) => file.type.includes(t)) ? undefined : msg ?? `File type must be: ${types.join(", ")}`;
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
function compose(...rules) {
|
|
1895
|
+
const hasAsync = rules.some((r) => r._isAsync);
|
|
1896
|
+
const fn = hasAsync ? async (value, context) => {
|
|
1897
|
+
for (const rule of rules) {
|
|
1898
|
+
const result = await rule(value, context);
|
|
1899
|
+
if (result)
|
|
1900
|
+
return result;
|
|
1901
|
+
}
|
|
1902
|
+
return;
|
|
1903
|
+
} : (value, context) => {
|
|
1904
|
+
for (const rule of rules) {
|
|
1905
|
+
const result = rule(value, context);
|
|
1906
|
+
if (result)
|
|
1907
|
+
return result;
|
|
1908
|
+
}
|
|
1909
|
+
return;
|
|
1910
|
+
};
|
|
1911
|
+
fn._isAsync = hasAsync;
|
|
1912
|
+
fn._isRequired = rules.some((r) => r._isRequired);
|
|
1913
|
+
fn._crossField = rules.some((r) => r._crossField);
|
|
1914
|
+
const maxDebounce = rules.reduce((max2, r) => Math.max(max2, r._debounce ?? 0), 0);
|
|
1915
|
+
if (maxDebounce > 0)
|
|
1916
|
+
fn._debounce = maxDebounce;
|
|
1917
|
+
return fn;
|
|
1918
|
+
}
|
|
1919
|
+
function when(condition, rule) {
|
|
1920
|
+
const fn = rule._isAsync ? async (value, context) => {
|
|
1921
|
+
if (!condition(value, context))
|
|
1922
|
+
return;
|
|
1923
|
+
return rule(value, context);
|
|
1924
|
+
} : (value, context) => {
|
|
1925
|
+
if (!condition(value, context))
|
|
1926
|
+
return;
|
|
1927
|
+
return rule(value, context);
|
|
1928
|
+
};
|
|
1929
|
+
fn._isAsync = rule._isAsync;
|
|
1930
|
+
fn._crossField = rule._crossField;
|
|
1931
|
+
return fn;
|
|
1932
|
+
}
|
|
1933
|
+
var v = {
|
|
1934
|
+
required,
|
|
1935
|
+
email,
|
|
1936
|
+
url,
|
|
1937
|
+
date,
|
|
1938
|
+
phone,
|
|
1939
|
+
minLength,
|
|
1940
|
+
maxLength,
|
|
1941
|
+
min,
|
|
1942
|
+
max,
|
|
1943
|
+
pattern,
|
|
1944
|
+
number,
|
|
1945
|
+
integer,
|
|
1946
|
+
positive,
|
|
1947
|
+
custom,
|
|
1948
|
+
async,
|
|
1949
|
+
matches,
|
|
1950
|
+
oneOf,
|
|
1951
|
+
notOneOf,
|
|
1952
|
+
fileSize,
|
|
1953
|
+
fileType,
|
|
1954
|
+
compose,
|
|
1955
|
+
when
|
|
1956
|
+
};
|
|
1957
|
+
// src/schema.ts
|
|
1958
|
+
function zodAdapter(zodSchema) {
|
|
1959
|
+
return {
|
|
1960
|
+
parse: (data) => zodSchema.parse(data),
|
|
1961
|
+
safeParse: (data) => {
|
|
1962
|
+
const result = zodSchema.safeParse(data);
|
|
1963
|
+
if (result.success) {
|
|
1964
|
+
return { success: true, data: result.data };
|
|
1965
|
+
}
|
|
1966
|
+
return {
|
|
1967
|
+
success: false,
|
|
1968
|
+
error: {
|
|
1969
|
+
issues: result.error.issues.map((issue) => ({
|
|
1970
|
+
path: issue.path,
|
|
1971
|
+
message: issue.message
|
|
1972
|
+
}))
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
function valibotAdapter(schema, parse, safeParse) {
|
|
1979
|
+
return {
|
|
1980
|
+
parse: (data) => parse(schema, data),
|
|
1981
|
+
safeParse: (data) => {
|
|
1982
|
+
const result = safeParse(schema, data);
|
|
1983
|
+
if (result.success) {
|
|
1984
|
+
return { success: true, data: result.output };
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
success: false,
|
|
1988
|
+
error: {
|
|
1989
|
+
issues: (result.issues || []).map((issue) => ({
|
|
1990
|
+
path: (issue.path || []).map((p) => p.key),
|
|
1991
|
+
message: issue.message
|
|
1992
|
+
}))
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function createSchemaValidator(opts) {
|
|
1999
|
+
return {
|
|
2000
|
+
parse: (data) => {
|
|
2001
|
+
const result = opts.validate(data);
|
|
2002
|
+
if (result.success)
|
|
2003
|
+
return result.data;
|
|
2004
|
+
throw new Error("Validation failed");
|
|
2005
|
+
},
|
|
2006
|
+
safeParse: (data) => {
|
|
2007
|
+
const result = opts.validate(data);
|
|
2008
|
+
if (result.success) {
|
|
2009
|
+
return { success: true, data: result.data };
|
|
2010
|
+
}
|
|
2011
|
+
return {
|
|
2012
|
+
success: false,
|
|
2013
|
+
error: {
|
|
2014
|
+
issues: Object.entries(result.errors).flatMap(([path, messages]) => messages.map((message) => ({
|
|
2015
|
+
path: path.split("."),
|
|
2016
|
+
message
|
|
2017
|
+
})))
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
var EREO_SCHEMA_MARKER = Symbol("ereo-schema");
|
|
2024
|
+
function ereoSchema(definition) {
|
|
2025
|
+
const schema = {
|
|
2026
|
+
[EREO_SCHEMA_MARKER]: true,
|
|
2027
|
+
parse: (data) => {
|
|
2028
|
+
const result = schema.safeParse(data);
|
|
2029
|
+
if (result.success)
|
|
2030
|
+
return result.data;
|
|
2031
|
+
const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
2032
|
+
throw new Error(`Validation failed:
|
|
2033
|
+
${messages.join(`
|
|
2034
|
+
`)}`);
|
|
2035
|
+
},
|
|
2036
|
+
safeParse: (data) => {
|
|
2037
|
+
const context = {
|
|
2038
|
+
getValue: (path) => getPath(data, path),
|
|
2039
|
+
getValues: () => data
|
|
2040
|
+
};
|
|
2041
|
+
const errors = validateDefinition(definition, data, "", context);
|
|
2042
|
+
if (errors.length === 0) {
|
|
2043
|
+
return { success: true, data };
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
success: false,
|
|
2047
|
+
error: { issues: errors }
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
return schema;
|
|
2052
|
+
}
|
|
2053
|
+
function validateDefinition(definition, data, basePath, context) {
|
|
2054
|
+
const issues = [];
|
|
2055
|
+
for (const [key, rule] of Object.entries(definition)) {
|
|
2056
|
+
const path = basePath ? `${basePath}.${key}` : key;
|
|
2057
|
+
const value = data?.[key];
|
|
2058
|
+
if (typeof rule === "function") {
|
|
2059
|
+
if (rule._isAsync)
|
|
2060
|
+
continue;
|
|
2061
|
+
const result = rule(value, context);
|
|
2062
|
+
if (typeof result === "string") {
|
|
2063
|
+
issues.push({ path: path.split("."), message: result });
|
|
2064
|
+
}
|
|
2065
|
+
} else if (Array.isArray(rule)) {
|
|
2066
|
+
for (const validator of rule) {
|
|
2067
|
+
if (typeof validator === "function") {
|
|
2068
|
+
if (validator._isAsync)
|
|
2069
|
+
continue;
|
|
2070
|
+
const result = validator(value, context);
|
|
2071
|
+
if (typeof result === "string") {
|
|
2072
|
+
issues.push({ path: path.split("."), message: result });
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
} else if (typeof rule === "object" && rule !== null) {
|
|
2078
|
+
const nested = validateDefinition(rule, value, path, context);
|
|
2079
|
+
issues.push(...nested);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
return issues;
|
|
2083
|
+
}
|
|
2084
|
+
function isEreoSchema(value) {
|
|
2085
|
+
return value !== null && typeof value === "object" && EREO_SCHEMA_MARKER in value;
|
|
2086
|
+
}
|
|
2087
|
+
function formDataToObject(formData, opts) {
|
|
2088
|
+
const result = {};
|
|
2089
|
+
const arrayFields = new Set(opts?.arrays ?? []);
|
|
2090
|
+
for (const [key, value] of formData.entries()) {
|
|
2091
|
+
const isArray = key.endsWith("[]") || arrayFields.has(key);
|
|
2092
|
+
const cleanKey = key.replace(/\[\]$/, "");
|
|
2093
|
+
const coerced = opts?.coerce !== false ? coerceValue(value) : value;
|
|
2094
|
+
if (isArray) {
|
|
2095
|
+
if (!result[cleanKey])
|
|
2096
|
+
result[cleanKey] = [];
|
|
2097
|
+
result[cleanKey].push(coerced);
|
|
2098
|
+
} else if (cleanKey.includes(".") || cleanKey.includes("[")) {
|
|
2099
|
+
setNestedValue(result, cleanKey, coerced);
|
|
2100
|
+
} else {
|
|
2101
|
+
result[cleanKey] = coerced;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return result;
|
|
2105
|
+
}
|
|
2106
|
+
function coerceValue(value) {
|
|
2107
|
+
if (value instanceof File)
|
|
2108
|
+
return value;
|
|
2109
|
+
const str = String(value);
|
|
2110
|
+
if (str === "true")
|
|
2111
|
+
return true;
|
|
2112
|
+
if (str === "false")
|
|
2113
|
+
return false;
|
|
2114
|
+
if (str === "null")
|
|
2115
|
+
return null;
|
|
2116
|
+
if (str === "")
|
|
2117
|
+
return "";
|
|
2118
|
+
const trimmed = str.trim();
|
|
2119
|
+
if (trimmed !== "" && !/^0\d/.test(trimmed)) {
|
|
2120
|
+
const num = Number(trimmed);
|
|
2121
|
+
if (!isNaN(num))
|
|
2122
|
+
return num;
|
|
2123
|
+
}
|
|
2124
|
+
if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/.test(str)) {
|
|
2125
|
+
const d = new Date(str);
|
|
2126
|
+
if (!isNaN(d.getTime()))
|
|
2127
|
+
return d.toISOString();
|
|
2128
|
+
}
|
|
2129
|
+
return str;
|
|
2130
|
+
}
|
|
2131
|
+
function setNestedValue(obj, path, value) {
|
|
2132
|
+
const segments = [];
|
|
2133
|
+
let current = "";
|
|
2134
|
+
for (let i = 0;i < path.length; i++) {
|
|
2135
|
+
const char = path[i];
|
|
2136
|
+
if (char === ".") {
|
|
2137
|
+
if (current) {
|
|
2138
|
+
segments.push(current);
|
|
2139
|
+
current = "";
|
|
2140
|
+
}
|
|
2141
|
+
} else if (char === "[") {
|
|
2142
|
+
if (current) {
|
|
2143
|
+
segments.push(current);
|
|
2144
|
+
current = "";
|
|
2145
|
+
}
|
|
2146
|
+
const close = path.indexOf("]", i);
|
|
2147
|
+
if (close !== -1) {
|
|
2148
|
+
const idx = path.slice(i + 1, close);
|
|
2149
|
+
const num = parseInt(idx, 10);
|
|
2150
|
+
segments.push(!isNaN(num) ? num : idx);
|
|
2151
|
+
i = close;
|
|
2152
|
+
}
|
|
2153
|
+
} else {
|
|
2154
|
+
current += char;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (current)
|
|
2158
|
+
segments.push(current);
|
|
2159
|
+
let target = obj;
|
|
2160
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
2161
|
+
const seg = segments[i];
|
|
2162
|
+
const nextSeg = segments[i + 1];
|
|
2163
|
+
if (target[seg] === undefined) {
|
|
2164
|
+
target[seg] = typeof nextSeg === "number" ? [] : {};
|
|
2165
|
+
}
|
|
2166
|
+
target = target[seg];
|
|
2167
|
+
}
|
|
2168
|
+
target[segments[segments.length - 1]] = value;
|
|
2169
|
+
}
|
|
2170
|
+
// src/action.ts
|
|
2171
|
+
import { createElement as createElement3, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2, useState } from "react";
|
|
2172
|
+
import { batch as batch2 } from "@ereo/state";
|
|
2173
|
+
function createFormAction(opts) {
|
|
2174
|
+
return async (request) => {
|
|
2175
|
+
try {
|
|
2176
|
+
const contentType = request.headers.get("Content-Type") || "";
|
|
2177
|
+
let raw;
|
|
2178
|
+
if (contentType.includes("application/json")) {
|
|
2179
|
+
raw = await request.json();
|
|
2180
|
+
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
2181
|
+
const formData = await request.formData();
|
|
2182
|
+
raw = formDataToObject(formData);
|
|
2183
|
+
} else {
|
|
2184
|
+
try {
|
|
2185
|
+
const text = await request.text();
|
|
2186
|
+
raw = JSON.parse(text);
|
|
2187
|
+
} catch {
|
|
2188
|
+
return { success: false, errors: { "": ["Invalid request body"] } };
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
let values;
|
|
2192
|
+
if (opts.schema) {
|
|
2193
|
+
if (opts.schema.safeParse) {
|
|
2194
|
+
const result = opts.schema.safeParse(raw);
|
|
2195
|
+
if (!result.success) {
|
|
2196
|
+
const errors = {};
|
|
2197
|
+
for (const issue of result.error.issues) {
|
|
2198
|
+
const path = issue.path.join(".");
|
|
2199
|
+
if (!errors[path])
|
|
2200
|
+
errors[path] = [];
|
|
2201
|
+
errors[path].push(issue.message);
|
|
2202
|
+
}
|
|
2203
|
+
return { success: false, errors };
|
|
2204
|
+
}
|
|
2205
|
+
values = result.data;
|
|
2206
|
+
} else {
|
|
2207
|
+
try {
|
|
2208
|
+
values = opts.schema.parse(raw);
|
|
2209
|
+
} catch (e) {
|
|
2210
|
+
if (e?.issues) {
|
|
2211
|
+
const errors = {};
|
|
2212
|
+
for (const issue of e.issues) {
|
|
2213
|
+
const path = issue.path?.join(".") ?? "";
|
|
2214
|
+
if (!errors[path])
|
|
2215
|
+
errors[path] = [];
|
|
2216
|
+
errors[path].push(issue.message);
|
|
2217
|
+
}
|
|
2218
|
+
return { success: false, errors };
|
|
2219
|
+
}
|
|
2220
|
+
return { success: false, errors: { "": [e?.message ?? "Validation failed"] } };
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
} else {
|
|
2224
|
+
values = raw;
|
|
2225
|
+
}
|
|
2226
|
+
const data = await opts.handler(values);
|
|
2227
|
+
return { success: true, data };
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
if (opts.onError) {
|
|
2230
|
+
return opts.onError(error);
|
|
2231
|
+
}
|
|
2232
|
+
return {
|
|
2233
|
+
success: false,
|
|
2234
|
+
errors: { "": [error instanceof Error ? error.message : "Server error"] }
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
function ActionForm(props) {
|
|
2240
|
+
const {
|
|
2241
|
+
form,
|
|
2242
|
+
action,
|
|
2243
|
+
method = "post",
|
|
2244
|
+
onSuccess,
|
|
2245
|
+
onError,
|
|
2246
|
+
children,
|
|
2247
|
+
className,
|
|
2248
|
+
id,
|
|
2249
|
+
encType = "application/json"
|
|
2250
|
+
} = props;
|
|
2251
|
+
const abortRef = useRef2(null);
|
|
2252
|
+
const generationRef = useRef2(0);
|
|
2253
|
+
useEffect2(() => {
|
|
2254
|
+
return () => {
|
|
2255
|
+
abortRef.current?.abort();
|
|
2256
|
+
};
|
|
2257
|
+
}, []);
|
|
2258
|
+
const handleSubmit = useCallback2(async (e) => {
|
|
2259
|
+
e.preventDefault();
|
|
2260
|
+
abortRef.current?.abort();
|
|
2261
|
+
const generation = ++generationRef.current;
|
|
2262
|
+
const controller = new AbortController;
|
|
2263
|
+
abortRef.current = controller;
|
|
2264
|
+
const valid = await form.validate();
|
|
2265
|
+
if (generation !== generationRef.current)
|
|
2266
|
+
return;
|
|
2267
|
+
if (!valid) {
|
|
2268
|
+
focusFirstError(form);
|
|
2269
|
+
announceErrors({}, { prefix: "Please fix the following errors:" });
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
const values = form.getValues();
|
|
2273
|
+
batch2(() => {
|
|
2274
|
+
form.isSubmitting.set(true);
|
|
2275
|
+
form.submitState.set("submitting");
|
|
2276
|
+
});
|
|
2277
|
+
announceSubmitStatus("submitting");
|
|
2278
|
+
try {
|
|
2279
|
+
let result;
|
|
2280
|
+
if (typeof action === "function") {
|
|
2281
|
+
result = await action(values);
|
|
2282
|
+
} else {
|
|
2283
|
+
const isMultipart = encType === "multipart/form-data";
|
|
2284
|
+
const response = await fetch(action, {
|
|
2285
|
+
method: method.toUpperCase(),
|
|
2286
|
+
headers: isMultipart ? undefined : { "Content-Type": "application/json" },
|
|
2287
|
+
body: isMultipart ? form.toFormData() : JSON.stringify(values),
|
|
2288
|
+
signal: controller.signal
|
|
2289
|
+
});
|
|
2290
|
+
result = await response.json();
|
|
2291
|
+
}
|
|
2292
|
+
if (generation !== generationRef.current)
|
|
2293
|
+
return;
|
|
2294
|
+
if (result.success) {
|
|
2295
|
+
batch2(() => {
|
|
2296
|
+
form.isSubmitting.set(false);
|
|
2297
|
+
form.submitState.set("success");
|
|
2298
|
+
form.submitCount.update((c) => c + 1);
|
|
2299
|
+
});
|
|
2300
|
+
announceSubmitStatus("success");
|
|
2301
|
+
onSuccess?.(result.data);
|
|
2302
|
+
} else {
|
|
2303
|
+
batch2(() => {
|
|
2304
|
+
form.isSubmitting.set(false);
|
|
2305
|
+
form.submitState.set("error");
|
|
2306
|
+
});
|
|
2307
|
+
announceSubmitStatus("error");
|
|
2308
|
+
if (result.errors) {
|
|
2309
|
+
for (const [path, errors] of Object.entries(result.errors)) {
|
|
2310
|
+
if (path) {
|
|
2311
|
+
form.setErrors(path, errors);
|
|
2312
|
+
} else {
|
|
2313
|
+
form.setFormErrors(errors);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
focusFirstError(form);
|
|
2317
|
+
announceErrors(result.errors);
|
|
2318
|
+
onError?.(result.errors);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
if (generation !== generationRef.current || controller.signal.aborted)
|
|
2323
|
+
return;
|
|
2324
|
+
batch2(() => {
|
|
2325
|
+
form.isSubmitting.set(false);
|
|
2326
|
+
form.submitState.set("error");
|
|
2327
|
+
});
|
|
2328
|
+
announceSubmitStatus("error");
|
|
2329
|
+
const errorMsg = error instanceof Error ? error.message : "Submission failed";
|
|
2330
|
+
form.setFormErrors([errorMsg]);
|
|
2331
|
+
onError?.({ "": [errorMsg] });
|
|
2332
|
+
}
|
|
2333
|
+
}, [form, action, method, encType, onSuccess, onError]);
|
|
2334
|
+
return createElement3("form", {
|
|
2335
|
+
id,
|
|
2336
|
+
className,
|
|
2337
|
+
method,
|
|
2338
|
+
action: typeof action === "string" ? action : undefined,
|
|
2339
|
+
onSubmit: handleSubmit,
|
|
2340
|
+
noValidate: true
|
|
2341
|
+
}, children);
|
|
2342
|
+
}
|
|
2343
|
+
function useFormAction(opts) {
|
|
2344
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
2345
|
+
const [result, setResult] = useState(null);
|
|
2346
|
+
const abortRef = useRef2(null);
|
|
2347
|
+
const cancel = useCallback2(() => {
|
|
2348
|
+
abortRef.current?.abort();
|
|
2349
|
+
abortRef.current = null;
|
|
2350
|
+
setIsSubmitting(false);
|
|
2351
|
+
}, []);
|
|
2352
|
+
const submit = useCallback2(async (values) => {
|
|
2353
|
+
cancel();
|
|
2354
|
+
const controller = new AbortController;
|
|
2355
|
+
abortRef.current = controller;
|
|
2356
|
+
setIsSubmitting(true);
|
|
2357
|
+
try {
|
|
2358
|
+
let actionResult;
|
|
2359
|
+
if (typeof opts.action === "function") {
|
|
2360
|
+
actionResult = await opts.action(values);
|
|
2361
|
+
} else {
|
|
2362
|
+
const isMultipart = opts.encType === "multipart/form-data";
|
|
2363
|
+
const response = await fetch(opts.action, {
|
|
2364
|
+
method: (opts.method ?? "POST").toUpperCase(),
|
|
2365
|
+
headers: isMultipart ? undefined : { "Content-Type": "application/json" },
|
|
2366
|
+
body: isMultipart ? objectToFormData(values) : JSON.stringify(values),
|
|
2367
|
+
signal: controller.signal
|
|
2368
|
+
});
|
|
2369
|
+
actionResult = await response.json();
|
|
2370
|
+
}
|
|
2371
|
+
if (!controller.signal.aborted) {
|
|
2372
|
+
setResult(actionResult);
|
|
2373
|
+
setIsSubmitting(false);
|
|
2374
|
+
}
|
|
2375
|
+
return actionResult;
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
if (!controller.signal.aborted) {
|
|
2378
|
+
const errorResult = {
|
|
2379
|
+
success: false,
|
|
2380
|
+
errors: {
|
|
2381
|
+
"": [error instanceof Error ? error.message : "Request failed"]
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
setResult(errorResult);
|
|
2385
|
+
setIsSubmitting(false);
|
|
2386
|
+
return errorResult;
|
|
2387
|
+
}
|
|
2388
|
+
return { success: false, errors: { "": ["Request cancelled"] } };
|
|
2389
|
+
}
|
|
2390
|
+
}, [opts.action, opts.method, opts.encType, cancel]);
|
|
2391
|
+
return { submit, cancel, isSubmitting, result };
|
|
2392
|
+
}
|
|
2393
|
+
function parseActionResult(response) {
|
|
2394
|
+
if (response === null || response === undefined) {
|
|
2395
|
+
return { success: false, errors: { "": ["Empty response"] } };
|
|
2396
|
+
}
|
|
2397
|
+
if (typeof response === "object") {
|
|
2398
|
+
const obj = response;
|
|
2399
|
+
if ("success" in obj) {
|
|
2400
|
+
return obj;
|
|
2401
|
+
}
|
|
2402
|
+
if ("error" in obj) {
|
|
2403
|
+
const error = obj.error;
|
|
2404
|
+
return {
|
|
2405
|
+
success: false,
|
|
2406
|
+
errors: {
|
|
2407
|
+
"": [typeof error === "string" ? error : "Unknown error"]
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
if ("errors" in obj) {
|
|
2412
|
+
return {
|
|
2413
|
+
success: false,
|
|
2414
|
+
errors: obj.errors
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
if ("data" in obj) {
|
|
2418
|
+
return { success: true, data: obj.data };
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return { success: true, data: response };
|
|
2422
|
+
}
|
|
2423
|
+
function objectToFormData(values) {
|
|
2424
|
+
const fd = new FormData;
|
|
2425
|
+
if (values === null || typeof values !== "object")
|
|
2426
|
+
return fd;
|
|
2427
|
+
const flat = flattenToPaths(values);
|
|
2428
|
+
for (const [path, value] of flat) {
|
|
2429
|
+
if (value instanceof File) {
|
|
2430
|
+
fd.append(path, value);
|
|
2431
|
+
} else if (typeof value !== "object" || value === null) {
|
|
2432
|
+
fd.append(path, String(value ?? ""));
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
return fd;
|
|
2436
|
+
}
|
|
2437
|
+
// src/wizard.ts
|
|
2438
|
+
import {
|
|
2439
|
+
createElement as createElement4,
|
|
2440
|
+
createContext as createContext2,
|
|
2441
|
+
useContext as useContext2,
|
|
2442
|
+
useRef as useRef3,
|
|
2443
|
+
useCallback as useCallback3,
|
|
2444
|
+
useEffect as useEffect3,
|
|
2445
|
+
useMemo as useMemo3
|
|
2446
|
+
} from "react";
|
|
2447
|
+
import { signal as signal3, batch as batch3 } from "@ereo/state";
|
|
2448
|
+
import { useSignal as useSignal2 } from "@ereo/state";
|
|
2449
|
+
function createWizard(config) {
|
|
2450
|
+
const form = new FormStore(config.form);
|
|
2451
|
+
const currentStep = signal3(0);
|
|
2452
|
+
const completedSteps = signal3(new Set);
|
|
2453
|
+
const { steps, persist, persistKey = "ereo-wizard" } = config;
|
|
2454
|
+
if (persist && typeof window !== "undefined") {
|
|
2455
|
+
try {
|
|
2456
|
+
const storage = persist === "localStorage" ? localStorage : sessionStorage;
|
|
2457
|
+
const saved = storage.getItem(persistKey);
|
|
2458
|
+
if (saved) {
|
|
2459
|
+
const parsed = JSON.parse(saved);
|
|
2460
|
+
if (parsed.values) {
|
|
2461
|
+
form.resetTo(parsed.values);
|
|
2462
|
+
}
|
|
2463
|
+
if (typeof parsed.step === "number") {
|
|
2464
|
+
currentStep.set(parsed.step);
|
|
2465
|
+
}
|
|
2466
|
+
if (parsed.completed) {
|
|
2467
|
+
completedSteps.set(new Set(parsed.completed));
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
} catch {}
|
|
2471
|
+
}
|
|
2472
|
+
let persistTimer = null;
|
|
2473
|
+
function persistState() {
|
|
2474
|
+
if (!persist || typeof window === "undefined")
|
|
2475
|
+
return;
|
|
2476
|
+
if (persistTimer)
|
|
2477
|
+
clearTimeout(persistTimer);
|
|
2478
|
+
persistTimer = setTimeout(() => {
|
|
2479
|
+
try {
|
|
2480
|
+
const storage = persist === "localStorage" ? localStorage : sessionStorage;
|
|
2481
|
+
storage.setItem(persistKey, JSON.stringify({
|
|
2482
|
+
values: form._getCurrentValues(),
|
|
2483
|
+
step: currentStep.get(),
|
|
2484
|
+
completed: Array.from(completedSteps.get())
|
|
2485
|
+
}));
|
|
2486
|
+
} catch {}
|
|
2487
|
+
}, 300);
|
|
2488
|
+
}
|
|
2489
|
+
const unsubForm = form.subscribe(persistState);
|
|
2490
|
+
const unsubStep = currentStep.subscribe(persistState);
|
|
2491
|
+
const unsubCompleted = completedSteps.subscribe(persistState);
|
|
2492
|
+
function getState() {
|
|
2493
|
+
const step = currentStep.get();
|
|
2494
|
+
const stepConfig = steps[step];
|
|
2495
|
+
return {
|
|
2496
|
+
currentStep: step,
|
|
2497
|
+
currentStepId: stepConfig?.id ?? "",
|
|
2498
|
+
completedSteps: completedSteps.get(),
|
|
2499
|
+
totalSteps: steps.length,
|
|
2500
|
+
isFirst: step === 0,
|
|
2501
|
+
isLast: step === steps.length - 1,
|
|
2502
|
+
progress: steps.length > 1 ? step / (steps.length - 1) : 1
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
async function validateCurrentStep() {
|
|
2506
|
+
const step = currentStep.get();
|
|
2507
|
+
const stepConfig = steps[step];
|
|
2508
|
+
if (!stepConfig)
|
|
2509
|
+
return true;
|
|
2510
|
+
if (stepConfig.fields) {
|
|
2511
|
+
for (const field of stepConfig.fields) {
|
|
2512
|
+
form.setTouched(field, true);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
if (stepConfig.validate) {
|
|
2516
|
+
const valid = await stepConfig.validate();
|
|
2517
|
+
if (!valid)
|
|
2518
|
+
return false;
|
|
2519
|
+
}
|
|
2520
|
+
if (stepConfig.fields && stepConfig.fields.length > 0) {
|
|
2521
|
+
const result = await form._validationEngine.validateFields(stepConfig.fields);
|
|
2522
|
+
return result.success;
|
|
2523
|
+
}
|
|
2524
|
+
return true;
|
|
2525
|
+
}
|
|
2526
|
+
async function next() {
|
|
2527
|
+
const valid = await validateCurrentStep();
|
|
2528
|
+
if (!valid)
|
|
2529
|
+
return false;
|
|
2530
|
+
const step = currentStep.get();
|
|
2531
|
+
const stepConfig = steps[step];
|
|
2532
|
+
batch3(() => {
|
|
2533
|
+
const completed = new Set(completedSteps.get());
|
|
2534
|
+
if (stepConfig)
|
|
2535
|
+
completed.add(stepConfig.id);
|
|
2536
|
+
completedSteps.set(completed);
|
|
2537
|
+
if (step < steps.length - 1) {
|
|
2538
|
+
currentStep.set(step + 1);
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
return true;
|
|
2542
|
+
}
|
|
2543
|
+
function prev() {
|
|
2544
|
+
const step = currentStep.get();
|
|
2545
|
+
if (step > 0) {
|
|
2546
|
+
currentStep.set(step - 1);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
function goTo(stepIdOrIndex) {
|
|
2550
|
+
if (typeof stepIdOrIndex === "number") {
|
|
2551
|
+
if (stepIdOrIndex >= 0 && stepIdOrIndex < steps.length) {
|
|
2552
|
+
currentStep.set(stepIdOrIndex);
|
|
2553
|
+
}
|
|
2554
|
+
} else {
|
|
2555
|
+
const idx = steps.findIndex((s) => s.id === stepIdOrIndex);
|
|
2556
|
+
if (idx !== -1) {
|
|
2557
|
+
currentStep.set(idx);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
async function submit() {
|
|
2562
|
+
const valid = await validateCurrentStep();
|
|
2563
|
+
if (!valid)
|
|
2564
|
+
return;
|
|
2565
|
+
const step = currentStep.get();
|
|
2566
|
+
const stepConfig = steps[step];
|
|
2567
|
+
if (stepConfig) {
|
|
2568
|
+
const completed = new Set(completedSteps.get());
|
|
2569
|
+
completed.add(stepConfig.id);
|
|
2570
|
+
completedSteps.set(completed);
|
|
2571
|
+
}
|
|
2572
|
+
if (config.onComplete) {
|
|
2573
|
+
await form.submitWith(config.onComplete);
|
|
2574
|
+
} else {
|
|
2575
|
+
await form.handleSubmit();
|
|
2576
|
+
}
|
|
2577
|
+
if (persist && typeof window !== "undefined") {
|
|
2578
|
+
try {
|
|
2579
|
+
const storage = persist === "localStorage" ? localStorage : sessionStorage;
|
|
2580
|
+
storage.removeItem(persistKey);
|
|
2581
|
+
} catch {}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
function reset() {
|
|
2585
|
+
if (persistTimer) {
|
|
2586
|
+
clearTimeout(persistTimer);
|
|
2587
|
+
persistTimer = null;
|
|
2588
|
+
}
|
|
2589
|
+
batch3(() => {
|
|
2590
|
+
form.reset();
|
|
2591
|
+
currentStep.set(0);
|
|
2592
|
+
completedSteps.set(new Set);
|
|
2593
|
+
});
|
|
2594
|
+
if (persist && typeof window !== "undefined") {
|
|
2595
|
+
try {
|
|
2596
|
+
const storage = persist === "localStorage" ? localStorage : sessionStorage;
|
|
2597
|
+
storage.removeItem(persistKey);
|
|
2598
|
+
} catch {}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
function dispose() {
|
|
2602
|
+
unsubForm();
|
|
2603
|
+
unsubStep();
|
|
2604
|
+
unsubCompleted();
|
|
2605
|
+
if (persistTimer) {
|
|
2606
|
+
clearTimeout(persistTimer);
|
|
2607
|
+
persistTimer = null;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
return {
|
|
2611
|
+
form,
|
|
2612
|
+
currentStep,
|
|
2613
|
+
completedSteps,
|
|
2614
|
+
get state() {
|
|
2615
|
+
return getState();
|
|
2616
|
+
},
|
|
2617
|
+
next,
|
|
2618
|
+
prev,
|
|
2619
|
+
goTo,
|
|
2620
|
+
submit,
|
|
2621
|
+
reset,
|
|
2622
|
+
dispose,
|
|
2623
|
+
getStepConfig: (index) => steps[index],
|
|
2624
|
+
canGoNext: () => currentStep.get() < steps.length - 1,
|
|
2625
|
+
canGoPrev: () => currentStep.get() > 0
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
function useWizard(config) {
|
|
2629
|
+
const wizardRef = useRef3(null);
|
|
2630
|
+
if (!wizardRef.current) {
|
|
2631
|
+
wizardRef.current = createWizard(config);
|
|
2632
|
+
}
|
|
2633
|
+
useEffect3(() => {
|
|
2634
|
+
return () => {
|
|
2635
|
+
wizardRef.current?.dispose();
|
|
2636
|
+
};
|
|
2637
|
+
}, []);
|
|
2638
|
+
const wizard = wizardRef.current;
|
|
2639
|
+
const step = useSignal2(wizard.currentStep);
|
|
2640
|
+
const completed = useSignal2(wizard.completedSteps);
|
|
2641
|
+
const currentStepState = useMemo3(() => ({
|
|
2642
|
+
currentStep: step,
|
|
2643
|
+
currentStepId: config.steps[step]?.id ?? "",
|
|
2644
|
+
completedSteps: completed,
|
|
2645
|
+
totalSteps: config.steps.length,
|
|
2646
|
+
isFirst: step === 0,
|
|
2647
|
+
isLast: step === config.steps.length - 1,
|
|
2648
|
+
progress: config.steps.length > 1 ? step / (config.steps.length - 1) : 1
|
|
2649
|
+
}), [step, completed, config.steps]);
|
|
2650
|
+
return { ...wizard, currentStepState };
|
|
2651
|
+
}
|
|
2652
|
+
var WizardContext = createContext2(null);
|
|
2653
|
+
function WizardProvider({
|
|
2654
|
+
wizard,
|
|
2655
|
+
children
|
|
2656
|
+
}) {
|
|
2657
|
+
return createElement4(WizardContext.Provider, { value: wizard }, children);
|
|
2658
|
+
}
|
|
2659
|
+
function useWizardContext() {
|
|
2660
|
+
return useContext2(WizardContext);
|
|
2661
|
+
}
|
|
2662
|
+
function WizardStep({
|
|
2663
|
+
id,
|
|
2664
|
+
wizard: wizardProp,
|
|
2665
|
+
keepMounted = false,
|
|
2666
|
+
children
|
|
2667
|
+
}) {
|
|
2668
|
+
const contextWizard = useWizardContext();
|
|
2669
|
+
const wizard = wizardProp ?? contextWizard;
|
|
2670
|
+
if (!wizard)
|
|
2671
|
+
throw new Error("WizardStep requires a wizard prop or WizardProvider");
|
|
2672
|
+
const currentStep = useSignal2(wizard.currentStep);
|
|
2673
|
+
const config = wizard.getStepConfig(currentStep);
|
|
2674
|
+
const isActive = config?.id === id;
|
|
2675
|
+
if (!isActive && !keepMounted)
|
|
2676
|
+
return null;
|
|
2677
|
+
return createElement4("div", {
|
|
2678
|
+
"data-wizard-step": id,
|
|
2679
|
+
"data-active": isActive,
|
|
2680
|
+
style: isActive ? undefined : { display: "none" },
|
|
2681
|
+
role: "tabpanel",
|
|
2682
|
+
"aria-hidden": !isActive
|
|
2683
|
+
}, children);
|
|
2684
|
+
}
|
|
2685
|
+
function WizardProgress({
|
|
2686
|
+
wizard: wizardProp,
|
|
2687
|
+
renderStep
|
|
2688
|
+
}) {
|
|
2689
|
+
const contextWizard = useWizardContext();
|
|
2690
|
+
const wizard = wizardProp ?? contextWizard;
|
|
2691
|
+
if (!wizard)
|
|
2692
|
+
throw new Error("WizardProgress requires a wizard prop or WizardProvider");
|
|
2693
|
+
const currentStep = useSignal2(wizard.currentStep);
|
|
2694
|
+
const completed = useSignal2(wizard.completedSteps);
|
|
2695
|
+
const state = wizard.state;
|
|
2696
|
+
const steps = [];
|
|
2697
|
+
for (let i = 0;i < state.totalSteps; i++) {
|
|
2698
|
+
const stepConfig = wizard.getStepConfig(i);
|
|
2699
|
+
if (!stepConfig)
|
|
2700
|
+
continue;
|
|
2701
|
+
const isActive = i === currentStep;
|
|
2702
|
+
const isCompleted = completed.has(stepConfig.id);
|
|
2703
|
+
if (renderStep) {
|
|
2704
|
+
steps.push(renderStep(stepConfig, i, { isActive, isCompleted }));
|
|
2705
|
+
} else {
|
|
2706
|
+
steps.push(createElement4("div", {
|
|
2707
|
+
key: stepConfig.id,
|
|
2708
|
+
"data-step": stepConfig.id,
|
|
2709
|
+
"data-active": isActive,
|
|
2710
|
+
"data-completed": isCompleted,
|
|
2711
|
+
role: "tab",
|
|
2712
|
+
"aria-selected": isActive,
|
|
2713
|
+
style: {
|
|
2714
|
+
fontWeight: isActive ? "bold" : "normal",
|
|
2715
|
+
opacity: isCompleted || isActive ? 1 : 0.5
|
|
2716
|
+
}
|
|
2717
|
+
}, stepConfig.id));
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return createElement4("div", {
|
|
2721
|
+
role: "tablist",
|
|
2722
|
+
"aria-label": "Form steps",
|
|
2723
|
+
"data-wizard-progress": true
|
|
2724
|
+
}, ...steps);
|
|
2725
|
+
}
|
|
2726
|
+
function WizardNavigation({
|
|
2727
|
+
wizard: wizardProp,
|
|
2728
|
+
backLabel = "Back",
|
|
2729
|
+
nextLabel = "Next",
|
|
2730
|
+
submitLabel = "Submit"
|
|
2731
|
+
}) {
|
|
2732
|
+
const contextWizard = useWizardContext();
|
|
2733
|
+
const wizard = wizardProp ?? contextWizard;
|
|
2734
|
+
if (!wizard)
|
|
2735
|
+
throw new Error("WizardNavigation requires a wizard prop or WizardProvider");
|
|
2736
|
+
const currentStep = useSignal2(wizard.currentStep);
|
|
2737
|
+
const state = wizard.state;
|
|
2738
|
+
const handleBack = useCallback3(() => wizard.prev(), [wizard]);
|
|
2739
|
+
const handleNext = useCallback3(() => wizard.next(), [wizard]);
|
|
2740
|
+
const handleSubmit = useCallback3(() => wizard.submit(), [wizard]);
|
|
2741
|
+
return createElement4("div", { "data-wizard-navigation": true }, !state.isFirst ? createElement4("button", { type: "button", onClick: handleBack }, backLabel) : null, state.isLast ? createElement4("button", { type: "button", onClick: handleSubmit }, submitLabel) : createElement4("button", { type: "button", onClick: handleNext }, nextLabel));
|
|
2742
|
+
}
|
|
2743
|
+
// src/composition.ts
|
|
2744
|
+
function deepMergeObjects(a, b) {
|
|
2745
|
+
const result = { ...a };
|
|
2746
|
+
for (const key of Object.keys(b)) {
|
|
2747
|
+
const aVal = a[key];
|
|
2748
|
+
const bVal = b[key];
|
|
2749
|
+
if (aVal !== null && bVal !== null && aVal !== undefined && bVal !== undefined && typeof aVal === "object" && typeof bVal === "object" && !Array.isArray(aVal) && !Array.isArray(bVal)) {
|
|
2750
|
+
result[key] = deepMergeObjects(aVal, bVal);
|
|
2751
|
+
} else {
|
|
2752
|
+
result[key] = bVal;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return result;
|
|
2756
|
+
}
|
|
2757
|
+
function mergeFormConfigs(configA, configB) {
|
|
2758
|
+
const mergedValidators = {};
|
|
2759
|
+
if (configA.validators) {
|
|
2760
|
+
for (const [key, val] of Object.entries(configA.validators)) {
|
|
2761
|
+
if (val)
|
|
2762
|
+
mergedValidators[key] = val;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (configB.validators) {
|
|
2766
|
+
for (const [key, val] of Object.entries(configB.validators)) {
|
|
2767
|
+
if (val) {
|
|
2768
|
+
const existing = mergedValidators[key];
|
|
2769
|
+
if (existing) {
|
|
2770
|
+
const existingArr = Array.isArray(existing) ? existing : [existing];
|
|
2771
|
+
const newArr = Array.isArray(val) ? val : [val];
|
|
2772
|
+
mergedValidators[key] = [...existingArr, ...newArr];
|
|
2773
|
+
} else {
|
|
2774
|
+
mergedValidators[key] = val;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return {
|
|
2780
|
+
defaultValues: deepMergeObjects(configA.defaultValues, configB.defaultValues),
|
|
2781
|
+
onSubmit: configB.onSubmit ?? configA.onSubmit,
|
|
2782
|
+
schema: configB.schema ?? configA.schema,
|
|
2783
|
+
validators: Object.keys(mergedValidators).length > 0 ? mergedValidators : undefined,
|
|
2784
|
+
validateOn: configB.validateOn ?? configA.validateOn,
|
|
2785
|
+
validateOnMount: configB.validateOnMount ?? configA.validateOnMount,
|
|
2786
|
+
resetOnSubmit: configB.resetOnSubmit ?? configA.resetOnSubmit
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
function composeSchemas(prefix1, schema1, prefix2, schema2) {
|
|
2790
|
+
return {
|
|
2791
|
+
parse(data) {
|
|
2792
|
+
const obj = data ?? {};
|
|
2793
|
+
const result1 = schema1.parse(obj[prefix1]);
|
|
2794
|
+
const result2 = schema2.parse(obj[prefix2]);
|
|
2795
|
+
return { [prefix1]: result1, [prefix2]: result2 };
|
|
2796
|
+
},
|
|
2797
|
+
safeParse(data) {
|
|
2798
|
+
const allIssues = [];
|
|
2799
|
+
let result = {};
|
|
2800
|
+
const obj = data ?? {};
|
|
2801
|
+
if (schema1.safeParse) {
|
|
2802
|
+
const r1 = schema1.safeParse(obj[prefix1]);
|
|
2803
|
+
if (r1.success) {
|
|
2804
|
+
result[prefix1] = r1.data;
|
|
2805
|
+
} else {
|
|
2806
|
+
for (const issue of r1.error.issues) {
|
|
2807
|
+
allIssues.push({
|
|
2808
|
+
path: [prefix1, ...issue.path],
|
|
2809
|
+
message: issue.message
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
} else {
|
|
2814
|
+
try {
|
|
2815
|
+
result[prefix1] = schema1.parse(obj[prefix1]);
|
|
2816
|
+
} catch (e) {
|
|
2817
|
+
if (e?.issues) {
|
|
2818
|
+
for (const issue of e.issues) {
|
|
2819
|
+
allIssues.push({
|
|
2820
|
+
path: [prefix1, ...issue.path ?? []],
|
|
2821
|
+
message: issue.message
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
} else {
|
|
2825
|
+
throw e;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
if (schema2.safeParse) {
|
|
2830
|
+
const r2 = schema2.safeParse(obj[prefix2]);
|
|
2831
|
+
if (r2.success) {
|
|
2832
|
+
result[prefix2] = r2.data;
|
|
2833
|
+
} else {
|
|
2834
|
+
for (const issue of r2.error.issues) {
|
|
2835
|
+
allIssues.push({
|
|
2836
|
+
path: [prefix2, ...issue.path],
|
|
2837
|
+
message: issue.message
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
} else {
|
|
2842
|
+
try {
|
|
2843
|
+
result[prefix2] = schema2.parse(obj[prefix2]);
|
|
2844
|
+
} catch (e) {
|
|
2845
|
+
if (e?.issues) {
|
|
2846
|
+
for (const issue of e.issues) {
|
|
2847
|
+
allIssues.push({
|
|
2848
|
+
path: [prefix2, ...issue.path ?? []],
|
|
2849
|
+
message: issue.message
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
} else {
|
|
2853
|
+
throw e;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
if (allIssues.length > 0) {
|
|
2858
|
+
return { success: false, error: { issues: allIssues } };
|
|
2859
|
+
}
|
|
2860
|
+
return { success: true, data: result };
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
export {
|
|
2865
|
+
zodAdapter,
|
|
2866
|
+
when,
|
|
2867
|
+
valibotAdapter,
|
|
2868
|
+
v,
|
|
2869
|
+
useWizardContext,
|
|
2870
|
+
useWizard,
|
|
2871
|
+
useFormStatus,
|
|
2872
|
+
useFormContext,
|
|
2873
|
+
useFormAction,
|
|
2874
|
+
useForm,
|
|
2875
|
+
useFieldArray,
|
|
2876
|
+
useField,
|
|
2877
|
+
url,
|
|
2878
|
+
trapFocus,
|
|
2879
|
+
setPath,
|
|
2880
|
+
required,
|
|
2881
|
+
prefersReducedMotion,
|
|
2882
|
+
positive,
|
|
2883
|
+
phone,
|
|
2884
|
+
pattern,
|
|
2885
|
+
parsePath,
|
|
2886
|
+
parseActionResult,
|
|
2887
|
+
oneOf,
|
|
2888
|
+
number,
|
|
2889
|
+
notOneOf,
|
|
2890
|
+
minLength,
|
|
2891
|
+
min,
|
|
2892
|
+
mergeFormConfigs,
|
|
2893
|
+
maxLength,
|
|
2894
|
+
max,
|
|
2895
|
+
matches,
|
|
2896
|
+
isScreenReaderActive,
|
|
2897
|
+
isEreoSchema,
|
|
2898
|
+
integer,
|
|
2899
|
+
getPath,
|
|
2900
|
+
getLabelA11y,
|
|
2901
|
+
getFormA11y,
|
|
2902
|
+
getFieldsetA11y,
|
|
2903
|
+
getFieldWrapperA11y,
|
|
2904
|
+
getFieldA11y,
|
|
2905
|
+
getErrorSummaryA11y,
|
|
2906
|
+
getErrorA11y,
|
|
2907
|
+
getDescriptionA11y,
|
|
2908
|
+
generateA11yId,
|
|
2909
|
+
formDataToObject,
|
|
2910
|
+
focusFirstError,
|
|
2911
|
+
focusField,
|
|
2912
|
+
flattenToPaths,
|
|
2913
|
+
fileType,
|
|
2914
|
+
fileSize,
|
|
2915
|
+
ereoSchema,
|
|
2916
|
+
email,
|
|
2917
|
+
deepEqual,
|
|
2918
|
+
deepClone,
|
|
2919
|
+
date,
|
|
2920
|
+
custom,
|
|
2921
|
+
createWizard,
|
|
2922
|
+
createValuesProxy,
|
|
2923
|
+
createSchemaValidator,
|
|
2924
|
+
createFormStore,
|
|
2925
|
+
createFormAction,
|
|
2926
|
+
composeSchemas,
|
|
2927
|
+
compose,
|
|
2928
|
+
cleanupLiveRegion,
|
|
2929
|
+
async,
|
|
2930
|
+
announceSubmitStatus,
|
|
2931
|
+
announceErrors,
|
|
2932
|
+
announce,
|
|
2933
|
+
WizardStep,
|
|
2934
|
+
WizardProvider,
|
|
2935
|
+
WizardProgress,
|
|
2936
|
+
WizardNavigation,
|
|
2937
|
+
ValidationEngine,
|
|
2938
|
+
TextareaField,
|
|
2939
|
+
SelectField,
|
|
2940
|
+
FormStore,
|
|
2941
|
+
FormProvider,
|
|
2942
|
+
FieldArrayComponent as FieldArray,
|
|
2943
|
+
Field,
|
|
2944
|
+
ActionForm
|
|
2945
|
+
};
|