@ereo/forms 0.1.23 → 0.1.24
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/components.d.ts +5 -5
- package/dist/components.d.ts.map +1 -1
- package/dist/hooks.d.ts +7 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +781 -487
- package/dist/proxy.d.ts.map +1 -1
- package/dist/schema.d.ts +21 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/store.d.ts +32 -4
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +41 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/validation-engine.d.ts +7 -1
- package/dist/validation-engine.d.ts.map +1 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/wizard.d.ts +2 -2
- package/dist/wizard.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -219,6 +219,7 @@ function flattenToPaths(obj, prefix = "") {
|
|
|
219
219
|
var PROXY_MARKER = Symbol("ereo-form-proxy");
|
|
220
220
|
function createValuesProxy(store, basePath = "", proxyCache) {
|
|
221
221
|
const cache = proxyCache ?? new Map;
|
|
222
|
+
const s = store;
|
|
222
223
|
if (basePath && cache.has(basePath)) {
|
|
223
224
|
return cache.get(basePath);
|
|
224
225
|
}
|
|
@@ -229,7 +230,7 @@ function createValuesProxy(store, basePath = "", proxyCache) {
|
|
|
229
230
|
if (typeof prop === "symbol")
|
|
230
231
|
return;
|
|
231
232
|
const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
232
|
-
const value =
|
|
233
|
+
const value = s.getValue(fullPath);
|
|
233
234
|
if (value !== null && typeof value === "object") {
|
|
234
235
|
return createValuesProxy(store, fullPath, cache);
|
|
235
236
|
}
|
|
@@ -239,7 +240,7 @@ function createValuesProxy(store, basePath = "", proxyCache) {
|
|
|
239
240
|
if (typeof prop === "symbol")
|
|
240
241
|
return true;
|
|
241
242
|
const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
242
|
-
|
|
243
|
+
s.setValue(fullPath, value);
|
|
243
244
|
return true;
|
|
244
245
|
},
|
|
245
246
|
has(_target, prop) {
|
|
@@ -247,14 +248,14 @@ function createValuesProxy(store, basePath = "", proxyCache) {
|
|
|
247
248
|
return true;
|
|
248
249
|
if (typeof prop === "symbol")
|
|
249
250
|
return false;
|
|
250
|
-
const obj = basePath ?
|
|
251
|
+
const obj = basePath ? s.getValue(basePath) : s.getValues();
|
|
251
252
|
if (obj !== null && typeof obj === "object") {
|
|
252
253
|
return String(prop) in obj;
|
|
253
254
|
}
|
|
254
255
|
return false;
|
|
255
256
|
},
|
|
256
257
|
ownKeys() {
|
|
257
|
-
const obj = basePath ?
|
|
258
|
+
const obj = basePath ? s.getValue(basePath) : s.getValues();
|
|
258
259
|
if (obj !== null && typeof obj === "object") {
|
|
259
260
|
return Object.keys(obj);
|
|
260
261
|
}
|
|
@@ -263,14 +264,14 @@ function createValuesProxy(store, basePath = "", proxyCache) {
|
|
|
263
264
|
getOwnPropertyDescriptor(_target, prop) {
|
|
264
265
|
if (typeof prop === "symbol")
|
|
265
266
|
return;
|
|
266
|
-
const obj = basePath ?
|
|
267
|
+
const obj = basePath ? s.getValue(basePath) : s.getValues();
|
|
267
268
|
if (obj !== null && typeof obj === "object" && String(prop) in obj) {
|
|
268
269
|
const fullPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
269
270
|
return {
|
|
270
271
|
configurable: true,
|
|
271
272
|
enumerable: true,
|
|
272
273
|
writable: true,
|
|
273
|
-
value:
|
|
274
|
+
value: s.getValue(fullPath)
|
|
274
275
|
};
|
|
275
276
|
}
|
|
276
277
|
return;
|
|
@@ -285,6 +286,267 @@ function createValuesProxy(store, basePath = "", proxyCache) {
|
|
|
285
286
|
// src/validation-engine.ts
|
|
286
287
|
import { signal } from "@ereo/state";
|
|
287
288
|
|
|
289
|
+
// src/schema.ts
|
|
290
|
+
function isStandardSchema(value) {
|
|
291
|
+
return value !== null && typeof value === "object" && "~standard" in value;
|
|
292
|
+
}
|
|
293
|
+
function normalizePath(path) {
|
|
294
|
+
if (!path)
|
|
295
|
+
return [];
|
|
296
|
+
return path.map((segment) => {
|
|
297
|
+
if (typeof segment === "object" && segment !== null && "key" in segment) {
|
|
298
|
+
return typeof segment.key === "number" ? segment.key : String(segment.key);
|
|
299
|
+
}
|
|
300
|
+
return typeof segment === "number" ? segment : String(segment);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function standardSchemaAdapter(schema) {
|
|
304
|
+
return {
|
|
305
|
+
parse: (data) => {
|
|
306
|
+
const result = schema["~standard"].validate(data);
|
|
307
|
+
if (result instanceof Promise) {
|
|
308
|
+
throw new Error("Async Standard Schema validation not supported in parse()");
|
|
309
|
+
}
|
|
310
|
+
if ("issues" in result && result.issues) {
|
|
311
|
+
const msgs = result.issues.map((i) => i.message).join(", ");
|
|
312
|
+
throw new Error(msgs);
|
|
313
|
+
}
|
|
314
|
+
return result.value;
|
|
315
|
+
},
|
|
316
|
+
safeParse: (data) => {
|
|
317
|
+
const result = schema["~standard"].validate(data);
|
|
318
|
+
if (result instanceof Promise) {
|
|
319
|
+
throw new Error("Async Standard Schema validation not supported in safeParse()");
|
|
320
|
+
}
|
|
321
|
+
if ("issues" in result && result.issues) {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
error: {
|
|
325
|
+
issues: result.issues.map((issue) => ({
|
|
326
|
+
path: normalizePath(issue.path),
|
|
327
|
+
message: issue.message
|
|
328
|
+
}))
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return { success: true, data: result.value };
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function zodAdapter(zodSchema) {
|
|
337
|
+
return {
|
|
338
|
+
parse: (data) => zodSchema.parse(data),
|
|
339
|
+
safeParse: (data) => {
|
|
340
|
+
const result = zodSchema.safeParse(data);
|
|
341
|
+
if (result.success) {
|
|
342
|
+
return { success: true, data: result.data };
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
error: {
|
|
347
|
+
issues: result.error.issues.map((issue) => ({
|
|
348
|
+
path: issue.path,
|
|
349
|
+
message: issue.message
|
|
350
|
+
}))
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function valibotAdapter(schema, parse, safeParse) {
|
|
357
|
+
return {
|
|
358
|
+
parse: (data) => parse(schema, data),
|
|
359
|
+
safeParse: (data) => {
|
|
360
|
+
const result = safeParse(schema, data);
|
|
361
|
+
if (result.success) {
|
|
362
|
+
return { success: true, data: result.output };
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
success: false,
|
|
366
|
+
error: {
|
|
367
|
+
issues: (result.issues || []).map((issue) => ({
|
|
368
|
+
path: (issue.path || []).map((p) => p.key),
|
|
369
|
+
message: issue.message
|
|
370
|
+
}))
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function createSchemaValidator(opts) {
|
|
377
|
+
return {
|
|
378
|
+
parse: (data) => {
|
|
379
|
+
const result = opts.validate(data);
|
|
380
|
+
if (result.success)
|
|
381
|
+
return result.data;
|
|
382
|
+
throw new Error("Validation failed");
|
|
383
|
+
},
|
|
384
|
+
safeParse: (data) => {
|
|
385
|
+
const result = opts.validate(data);
|
|
386
|
+
if (result.success) {
|
|
387
|
+
return { success: true, data: result.data };
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
success: false,
|
|
391
|
+
error: {
|
|
392
|
+
issues: Object.entries(result.errors).flatMap(([path, messages]) => messages.map((message) => ({
|
|
393
|
+
path: path.split("."),
|
|
394
|
+
message
|
|
395
|
+
})))
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
var EREO_SCHEMA_MARKER = Symbol("ereo-schema");
|
|
402
|
+
function ereoSchema(definition) {
|
|
403
|
+
const schema = {
|
|
404
|
+
[EREO_SCHEMA_MARKER]: true,
|
|
405
|
+
parse: (data) => {
|
|
406
|
+
const result = schema.safeParse(data);
|
|
407
|
+
if (result.success)
|
|
408
|
+
return result.data;
|
|
409
|
+
const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
410
|
+
throw new Error(`Validation failed:
|
|
411
|
+
${messages.join(`
|
|
412
|
+
`)}`);
|
|
413
|
+
},
|
|
414
|
+
safeParse: (data) => {
|
|
415
|
+
const context = {
|
|
416
|
+
getValue: (path) => getPath(data, path),
|
|
417
|
+
getValues: () => data
|
|
418
|
+
};
|
|
419
|
+
const errors = validateDefinition(definition, data, "", context);
|
|
420
|
+
if (errors.length === 0) {
|
|
421
|
+
return { success: true, data };
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
error: { issues: errors }
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
return schema;
|
|
430
|
+
}
|
|
431
|
+
function validateDefinition(definition, data, basePath, context) {
|
|
432
|
+
const issues = [];
|
|
433
|
+
for (const [key, rule] of Object.entries(definition)) {
|
|
434
|
+
const path = basePath ? `${basePath}.${key}` : key;
|
|
435
|
+
const value = data?.[key];
|
|
436
|
+
if (typeof rule === "function") {
|
|
437
|
+
if (rule._isAsync)
|
|
438
|
+
continue;
|
|
439
|
+
const result = rule(value, context);
|
|
440
|
+
if (typeof result === "string") {
|
|
441
|
+
issues.push({ path: path.split("."), message: result });
|
|
442
|
+
}
|
|
443
|
+
} else if (Array.isArray(rule)) {
|
|
444
|
+
for (const validator of rule) {
|
|
445
|
+
if (typeof validator === "function") {
|
|
446
|
+
if (validator._isAsync)
|
|
447
|
+
continue;
|
|
448
|
+
const result = validator(value, context);
|
|
449
|
+
if (typeof result === "string") {
|
|
450
|
+
issues.push({ path: path.split("."), message: result });
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} else if (typeof rule === "object" && rule !== null) {
|
|
456
|
+
const nested = validateDefinition(rule, value, path, context);
|
|
457
|
+
issues.push(...nested);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return issues;
|
|
461
|
+
}
|
|
462
|
+
function isEreoSchema(value) {
|
|
463
|
+
return value !== null && typeof value === "object" && EREO_SCHEMA_MARKER in value;
|
|
464
|
+
}
|
|
465
|
+
function formDataToObject(formData, opts) {
|
|
466
|
+
const result = {};
|
|
467
|
+
const arrayFields = new Set(opts?.arrays ?? []);
|
|
468
|
+
for (const [key, value] of formData.entries()) {
|
|
469
|
+
const isArray = key.endsWith("[]") || arrayFields.has(key);
|
|
470
|
+
const cleanKey = key.replace(/\[\]$/, "");
|
|
471
|
+
const coerced = opts?.coerce !== false ? coerceValue(value) : value;
|
|
472
|
+
if (isArray) {
|
|
473
|
+
if (!result[cleanKey])
|
|
474
|
+
result[cleanKey] = [];
|
|
475
|
+
result[cleanKey].push(coerced);
|
|
476
|
+
} else if (cleanKey.includes(".") || cleanKey.includes("[")) {
|
|
477
|
+
setNestedValue(result, cleanKey, coerced);
|
|
478
|
+
} else {
|
|
479
|
+
result[cleanKey] = coerced;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
function coerceValue(value) {
|
|
485
|
+
if (value instanceof File)
|
|
486
|
+
return value;
|
|
487
|
+
const str = String(value);
|
|
488
|
+
if (str === "true")
|
|
489
|
+
return true;
|
|
490
|
+
if (str === "false")
|
|
491
|
+
return false;
|
|
492
|
+
if (str === "null")
|
|
493
|
+
return null;
|
|
494
|
+
if (str === "")
|
|
495
|
+
return "";
|
|
496
|
+
const trimmed = str.trim();
|
|
497
|
+
if (trimmed !== "" && !/^0\d/.test(trimmed)) {
|
|
498
|
+
const num = Number(trimmed);
|
|
499
|
+
if (!isNaN(num))
|
|
500
|
+
return num;
|
|
501
|
+
}
|
|
502
|
+
if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/.test(str)) {
|
|
503
|
+
const d = new Date(str);
|
|
504
|
+
if (!isNaN(d.getTime()))
|
|
505
|
+
return d.toISOString();
|
|
506
|
+
}
|
|
507
|
+
return str;
|
|
508
|
+
}
|
|
509
|
+
function setNestedValue(obj, path, value) {
|
|
510
|
+
const segments = [];
|
|
511
|
+
let current = "";
|
|
512
|
+
for (let i = 0;i < path.length; i++) {
|
|
513
|
+
const char = path[i];
|
|
514
|
+
if (char === ".") {
|
|
515
|
+
if (current) {
|
|
516
|
+
segments.push(current);
|
|
517
|
+
current = "";
|
|
518
|
+
}
|
|
519
|
+
} else if (char === "[") {
|
|
520
|
+
if (current) {
|
|
521
|
+
segments.push(current);
|
|
522
|
+
current = "";
|
|
523
|
+
}
|
|
524
|
+
const close = path.indexOf("]", i);
|
|
525
|
+
if (close !== -1) {
|
|
526
|
+
const idx = path.slice(i + 1, close);
|
|
527
|
+
const num = parseInt(idx, 10);
|
|
528
|
+
segments.push(!isNaN(num) ? num : idx);
|
|
529
|
+
i = close;
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
current += char;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (current)
|
|
536
|
+
segments.push(current);
|
|
537
|
+
let target = obj;
|
|
538
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
539
|
+
const seg = segments[i];
|
|
540
|
+
const nextSeg = segments[i + 1];
|
|
541
|
+
if (target[seg] === undefined) {
|
|
542
|
+
target[seg] = typeof nextSeg === "number" ? [] : {};
|
|
543
|
+
}
|
|
544
|
+
target = target[seg];
|
|
545
|
+
}
|
|
546
|
+
target[segments[segments.length - 1]] = value;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/validation-engine.ts
|
|
288
550
|
class ValidationEngine {
|
|
289
551
|
_store;
|
|
290
552
|
_fieldValidations = new Map;
|
|
@@ -294,6 +556,8 @@ class ValidationEngine {
|
|
|
294
556
|
_validatingSignals = new Map;
|
|
295
557
|
_fieldGenerations = new Map;
|
|
296
558
|
_validateAllController = null;
|
|
559
|
+
_dependents = new Map;
|
|
560
|
+
_validatingDependents = new Set;
|
|
297
561
|
constructor(store) {
|
|
298
562
|
this._store = store;
|
|
299
563
|
const { validators } = store.config;
|
|
@@ -305,14 +569,36 @@ class ValidationEngine {
|
|
|
305
569
|
}
|
|
306
570
|
}
|
|
307
571
|
}
|
|
572
|
+
const { dependencies } = store.config;
|
|
573
|
+
if (dependencies) {
|
|
574
|
+
for (const [dependent, sources] of Object.entries(dependencies)) {
|
|
575
|
+
if (sources) {
|
|
576
|
+
const sourceArr = Array.isArray(sources) ? sources : [sources];
|
|
577
|
+
for (const source of sourceArr) {
|
|
578
|
+
this._registerDependency(dependent, source);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
308
583
|
}
|
|
309
|
-
registerFieldValidators(path, validators, explicitTrigger) {
|
|
584
|
+
registerFieldValidators(path, validators, explicitTrigger, dependsOn) {
|
|
310
585
|
const derivedTrigger = explicitTrigger ?? this._deriveValidateOn(validators);
|
|
311
586
|
this._fieldValidations.set(path, {
|
|
312
587
|
validators,
|
|
313
588
|
validateOn: explicitTrigger,
|
|
314
589
|
derivedTrigger
|
|
315
590
|
});
|
|
591
|
+
for (const v of validators) {
|
|
592
|
+
if (v._dependsOnField) {
|
|
593
|
+
this._registerDependency(path, v._dependsOnField);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (dependsOn) {
|
|
597
|
+
const deps = Array.isArray(dependsOn) ? dependsOn : [dependsOn];
|
|
598
|
+
for (const dep of deps) {
|
|
599
|
+
this._registerDependency(path, dep);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
316
602
|
}
|
|
317
603
|
_deriveValidateOn(validators) {
|
|
318
604
|
const hasAsync = validators.some((v) => v._isAsync);
|
|
@@ -325,17 +611,18 @@ class ValidationEngine {
|
|
|
325
611
|
}
|
|
326
612
|
onFieldChange(path) {
|
|
327
613
|
const validation = this._fieldValidations.get(path);
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
614
|
+
if (validation) {
|
|
615
|
+
const trigger = validation.validateOn ?? validation.derivedTrigger;
|
|
616
|
+
if (trigger === "change") {
|
|
617
|
+
const debounceMs = this._getDebounceMs(validation.validators);
|
|
618
|
+
if (debounceMs > 0) {
|
|
619
|
+
this._debounceValidation(path, debounceMs);
|
|
620
|
+
} else {
|
|
621
|
+
this._runFieldValidation(path);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
338
624
|
}
|
|
625
|
+
this._triggerDependents(path);
|
|
339
626
|
}
|
|
340
627
|
onFieldBlur(path) {
|
|
341
628
|
const validation = this._fieldValidations.get(path);
|
|
@@ -382,16 +669,27 @@ class ValidationEngine {
|
|
|
382
669
|
const generation = (this._fieldGenerations.get(path) ?? 0) + 1;
|
|
383
670
|
this._fieldGenerations.set(path, generation);
|
|
384
671
|
this._setFieldValidating(path, true);
|
|
385
|
-
const
|
|
672
|
+
const syncErrors = [];
|
|
673
|
+
const asyncErrors = [];
|
|
386
674
|
const value = this._store.getValue(path);
|
|
387
675
|
const context = this._createContext(controller.signal);
|
|
388
676
|
try {
|
|
389
|
-
|
|
677
|
+
const syncValidators = validation.validators.filter((v) => !v._isAsync);
|
|
678
|
+
const asyncValidators = validation.validators.filter((v) => v._isAsync);
|
|
679
|
+
for (const validator of syncValidators) {
|
|
390
680
|
if (controller.signal.aborted)
|
|
391
681
|
break;
|
|
392
682
|
const result = await validator(value, context);
|
|
393
|
-
if (result)
|
|
394
|
-
|
|
683
|
+
if (result)
|
|
684
|
+
syncErrors.push(result);
|
|
685
|
+
}
|
|
686
|
+
if (syncErrors.length === 0 && !controller.signal.aborted) {
|
|
687
|
+
for (const validator of asyncValidators) {
|
|
688
|
+
if (controller.signal.aborted)
|
|
689
|
+
break;
|
|
690
|
+
const result = await validator(value, context);
|
|
691
|
+
if (result)
|
|
692
|
+
asyncErrors.push(result);
|
|
395
693
|
}
|
|
396
694
|
}
|
|
397
695
|
} finally {
|
|
@@ -400,9 +698,21 @@ class ValidationEngine {
|
|
|
400
698
|
this._abortControllers.delete(path);
|
|
401
699
|
}
|
|
402
700
|
}
|
|
701
|
+
const allErrors = [...syncErrors, ...asyncErrors];
|
|
403
702
|
if (!controller.signal.aborted && this._fieldGenerations.get(path) === generation) {
|
|
404
|
-
this._store.
|
|
405
|
-
|
|
703
|
+
this._store.clearErrorsBySource(path, "sync");
|
|
704
|
+
this._store.clearErrorsBySource(path, "async");
|
|
705
|
+
if (syncErrors.length > 0) {
|
|
706
|
+
this._store.setErrorsWithSource(path, syncErrors, "sync");
|
|
707
|
+
}
|
|
708
|
+
if (asyncErrors.length > 0) {
|
|
709
|
+
this._store.setErrorsWithSource(path, asyncErrors, "async");
|
|
710
|
+
}
|
|
711
|
+
if (allErrors.length === 0) {
|
|
712
|
+
this._store.clearErrorsBySource(path, "sync");
|
|
713
|
+
this._store.clearErrorsBySource(path, "async");
|
|
714
|
+
}
|
|
715
|
+
return allErrors;
|
|
406
716
|
}
|
|
407
717
|
return [];
|
|
408
718
|
}
|
|
@@ -426,9 +736,10 @@ class ValidationEngine {
|
|
|
426
736
|
this._validateAllController = controller;
|
|
427
737
|
const allErrors = {};
|
|
428
738
|
let hasErrors = false;
|
|
739
|
+
let schemaResult = null;
|
|
429
740
|
const schema = this._store.config.schema;
|
|
430
741
|
if (schema) {
|
|
431
|
-
|
|
742
|
+
schemaResult = await this._validateSchema(schema);
|
|
432
743
|
if (controller.signal.aborted)
|
|
433
744
|
return { success: true };
|
|
434
745
|
if (!schemaResult.success && schemaResult.errors) {
|
|
@@ -448,13 +759,24 @@ class ValidationEngine {
|
|
|
448
759
|
const value = this._store.getValue(path);
|
|
449
760
|
const context = this._createContext(controller.signal);
|
|
450
761
|
const errors = [];
|
|
451
|
-
|
|
762
|
+
const syncValidators = validation.validators.filter((v) => !v._isAsync);
|
|
763
|
+
const asyncValidators = validation.validators.filter((v) => v._isAsync);
|
|
764
|
+
for (const validator of syncValidators) {
|
|
452
765
|
if (controller.signal.aborted)
|
|
453
766
|
break;
|
|
454
767
|
const result = await validator(value, context);
|
|
455
768
|
if (result)
|
|
456
769
|
errors.push(result);
|
|
457
770
|
}
|
|
771
|
+
if (errors.length === 0 && !controller.signal.aborted) {
|
|
772
|
+
for (const validator of asyncValidators) {
|
|
773
|
+
if (controller.signal.aborted)
|
|
774
|
+
break;
|
|
775
|
+
const result = await validator(value, context);
|
|
776
|
+
if (result)
|
|
777
|
+
errors.push(result);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
458
780
|
if (errors.length > 0 && !controller.signal.aborted) {
|
|
459
781
|
if (allErrors[path]) {
|
|
460
782
|
allErrors[path] = [...allErrors[path], ...errors];
|
|
@@ -470,15 +792,51 @@ class ValidationEngine {
|
|
|
470
792
|
this._validateAllController = null;
|
|
471
793
|
}
|
|
472
794
|
this._store.clearErrors();
|
|
795
|
+
if (schema) {
|
|
796
|
+
const schemaErrors = schemaResult?.errors ?? {};
|
|
797
|
+
for (const [path, errors] of Object.entries(schemaErrors)) {
|
|
798
|
+
this._store.setErrorsWithSource(path, errors, "schema");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
473
801
|
for (const [path, errors] of Object.entries(allErrors)) {
|
|
474
|
-
|
|
802
|
+
const schemaErrors = schemaResult?.errors ?? {};
|
|
803
|
+
if (schemaErrors[path]) {
|
|
804
|
+
const validation = this._fieldValidations.get(path);
|
|
805
|
+
const hasAsync = validation?.validators.some((v) => v._isAsync);
|
|
806
|
+
const source = hasAsync && errors.length > 0 ? "sync" : "sync";
|
|
807
|
+
const fieldOnlyErrors = errors.filter((e) => !schemaErrors[path]?.includes(e));
|
|
808
|
+
if (fieldOnlyErrors.length > 0) {
|
|
809
|
+
this._store.setErrorsWithSource(path, fieldOnlyErrors, source);
|
|
810
|
+
}
|
|
811
|
+
} else {
|
|
812
|
+
const validation = this._fieldValidations.get(path);
|
|
813
|
+
if (validation) {
|
|
814
|
+
const syncValidators = validation.validators.filter((v) => !v._isAsync);
|
|
815
|
+
const asyncValidators = validation.validators.filter((v) => v._isAsync);
|
|
816
|
+
if (syncValidators.length > 0 && errors.length > 0) {
|
|
817
|
+
this._store.setErrorsWithSource(path, errors, "sync");
|
|
818
|
+
} else if (asyncValidators.length > 0 && errors.length > 0) {
|
|
819
|
+
this._store.setErrorsWithSource(path, errors, "async");
|
|
820
|
+
} else {
|
|
821
|
+
this._store.setErrors(path, errors);
|
|
822
|
+
}
|
|
823
|
+
} else {
|
|
824
|
+
this._store.setErrors(path, errors);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
475
827
|
}
|
|
476
828
|
return { success: !hasErrors, errors: hasErrors ? allErrors : undefined };
|
|
477
829
|
}
|
|
478
830
|
async _validateSchema(schema) {
|
|
831
|
+
let resolvedSchema;
|
|
832
|
+
if (isStandardSchema(schema)) {
|
|
833
|
+
resolvedSchema = standardSchemaAdapter(schema);
|
|
834
|
+
} else {
|
|
835
|
+
resolvedSchema = schema;
|
|
836
|
+
}
|
|
479
837
|
const values = this._store._getCurrentValues();
|
|
480
|
-
if (
|
|
481
|
-
const result =
|
|
838
|
+
if (resolvedSchema.safeParse) {
|
|
839
|
+
const result = resolvedSchema.safeParse(values);
|
|
482
840
|
if (result.success) {
|
|
483
841
|
return { success: true };
|
|
484
842
|
}
|
|
@@ -494,7 +852,7 @@ class ValidationEngine {
|
|
|
494
852
|
return { success: false, errors };
|
|
495
853
|
}
|
|
496
854
|
try {
|
|
497
|
-
|
|
855
|
+
resolvedSchema.parse(values);
|
|
498
856
|
return { success: true };
|
|
499
857
|
} catch (e) {
|
|
500
858
|
if (e?.issues) {
|
|
@@ -513,11 +871,48 @@ class ValidationEngine {
|
|
|
513
871
|
};
|
|
514
872
|
}
|
|
515
873
|
}
|
|
874
|
+
getRegisteredPaths() {
|
|
875
|
+
return this._fieldValidations.keys();
|
|
876
|
+
}
|
|
877
|
+
_registerDependency(dependentField, sourceField) {
|
|
878
|
+
let set = this._dependents.get(sourceField);
|
|
879
|
+
if (!set) {
|
|
880
|
+
set = new Set;
|
|
881
|
+
this._dependents.set(sourceField, set);
|
|
882
|
+
}
|
|
883
|
+
set.add(dependentField);
|
|
884
|
+
}
|
|
885
|
+
_triggerDependents(sourcePath) {
|
|
886
|
+
const dependents = this._dependents.get(sourcePath);
|
|
887
|
+
if (!dependents)
|
|
888
|
+
return;
|
|
889
|
+
for (const dep of dependents) {
|
|
890
|
+
if (this._validatingDependents.has(dep))
|
|
891
|
+
continue;
|
|
892
|
+
const validation = this._fieldValidations.get(dep);
|
|
893
|
+
if (!validation)
|
|
894
|
+
continue;
|
|
895
|
+
if (!this._store.getTouched(dep))
|
|
896
|
+
continue;
|
|
897
|
+
this._validatingDependents.add(dep);
|
|
898
|
+
this._runFieldValidation(dep);
|
|
899
|
+
Promise.resolve().then(() => {
|
|
900
|
+
this._validatingDependents.delete(dep);
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
getDependents(sourcePath) {
|
|
905
|
+
return this._dependents.get(sourcePath);
|
|
906
|
+
}
|
|
516
907
|
unregisterField(path) {
|
|
517
908
|
this._cancelFieldValidation(path);
|
|
518
909
|
this._fieldValidations.delete(path);
|
|
519
910
|
this._validatingSignals.delete(path);
|
|
520
911
|
this._fieldGenerations.delete(path);
|
|
912
|
+
this._dependents.delete(path);
|
|
913
|
+
for (const [, set] of this._dependents) {
|
|
914
|
+
set.delete(path);
|
|
915
|
+
}
|
|
521
916
|
}
|
|
522
917
|
dispose() {
|
|
523
918
|
for (const timer of this._debounceTimers.values()) {
|
|
@@ -531,6 +926,8 @@ class ValidationEngine {
|
|
|
531
926
|
this._validatingFields.clear();
|
|
532
927
|
this._validatingSignals.clear();
|
|
533
928
|
this._fieldGenerations.clear();
|
|
929
|
+
this._dependents.clear();
|
|
930
|
+
this._validatingDependents.clear();
|
|
534
931
|
if (this._validateAllController) {
|
|
535
932
|
this._validateAllController.abort();
|
|
536
933
|
this._validateAllController = null;
|
|
@@ -579,11 +976,203 @@ class ValidationEngine {
|
|
|
579
976
|
}
|
|
580
977
|
}
|
|
581
978
|
|
|
979
|
+
// src/a11y.ts
|
|
980
|
+
var idCounter = 0;
|
|
981
|
+
function generateA11yId(prefix = "ereo") {
|
|
982
|
+
return `${prefix}-${++idCounter}`;
|
|
983
|
+
}
|
|
984
|
+
function getFieldA11y(name, state) {
|
|
985
|
+
const attrs = {};
|
|
986
|
+
if (state.errors.length > 0 && state.touched) {
|
|
987
|
+
attrs["aria-invalid"] = true;
|
|
988
|
+
attrs["aria-describedby"] = `${name}-error`;
|
|
989
|
+
}
|
|
990
|
+
return attrs;
|
|
991
|
+
}
|
|
992
|
+
function getErrorA11y(name) {
|
|
993
|
+
return {
|
|
994
|
+
id: `${name}-error`,
|
|
995
|
+
role: "alert",
|
|
996
|
+
"aria-live": "polite"
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function getLabelA11y(name, opts) {
|
|
1000
|
+
return {
|
|
1001
|
+
htmlFor: name,
|
|
1002
|
+
id: opts?.id ?? `${name}-label`
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function getDescriptionA11y(name) {
|
|
1006
|
+
return {
|
|
1007
|
+
id: `${name}-description`
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function getFieldsetA11y(name, _legend) {
|
|
1011
|
+
return {
|
|
1012
|
+
role: "group",
|
|
1013
|
+
"aria-labelledby": `${name}-legend`
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function getFieldWrapperA11y(name, state) {
|
|
1017
|
+
const attrs = {};
|
|
1018
|
+
attrs["data-field"] = name;
|
|
1019
|
+
if (state.errors.length > 0 && state.touched) {
|
|
1020
|
+
attrs["data-invalid"] = true;
|
|
1021
|
+
}
|
|
1022
|
+
return attrs;
|
|
1023
|
+
}
|
|
1024
|
+
function getFormA11y(id, opts) {
|
|
1025
|
+
const attrs = {
|
|
1026
|
+
id,
|
|
1027
|
+
role: "form"
|
|
1028
|
+
};
|
|
1029
|
+
if (opts?.isSubmitting) {
|
|
1030
|
+
attrs["aria-busy"] = true;
|
|
1031
|
+
}
|
|
1032
|
+
return attrs;
|
|
1033
|
+
}
|
|
1034
|
+
function getErrorSummaryA11y(formId) {
|
|
1035
|
+
return {
|
|
1036
|
+
role: "alert",
|
|
1037
|
+
"aria-labelledby": `${formId}-error-summary`
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function focusFirstError(form) {
|
|
1041
|
+
if (typeof document === "undefined")
|
|
1042
|
+
return;
|
|
1043
|
+
const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
1044
|
+
const formStore = form;
|
|
1045
|
+
if (formStore._fieldRefs) {
|
|
1046
|
+
for (const [path, el] of formStore._fieldRefs) {
|
|
1047
|
+
if (!el)
|
|
1048
|
+
continue;
|
|
1049
|
+
const errors = form.getErrors(path).get();
|
|
1050
|
+
if (errors.length > 0) {
|
|
1051
|
+
el.focus();
|
|
1052
|
+
el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const elements = document.querySelectorAll('[aria-invalid="true"]');
|
|
1058
|
+
const first = elements[0];
|
|
1059
|
+
if (first) {
|
|
1060
|
+
first.focus();
|
|
1061
|
+
first.scrollIntoView({ behavior: scrollBehavior, block: "center" });
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function focusField(name) {
|
|
1065
|
+
if (typeof document === "undefined")
|
|
1066
|
+
return;
|
|
1067
|
+
const scrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
1068
|
+
const el = document.querySelector(`[name="${name}"]`);
|
|
1069
|
+
if (el) {
|
|
1070
|
+
el.focus();
|
|
1071
|
+
el.scrollIntoView({ behavior: scrollBehavior, block: "center" });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function trapFocus(container) {
|
|
1075
|
+
if (typeof document === "undefined")
|
|
1076
|
+
return () => {};
|
|
1077
|
+
const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
1078
|
+
const handleKeydown = (e) => {
|
|
1079
|
+
if (e.key !== "Tab")
|
|
1080
|
+
return;
|
|
1081
|
+
const focusable = container.querySelectorAll(focusableSelector);
|
|
1082
|
+
if (focusable.length === 0)
|
|
1083
|
+
return;
|
|
1084
|
+
const first = focusable[0];
|
|
1085
|
+
const last = focusable[focusable.length - 1];
|
|
1086
|
+
if (e.shiftKey) {
|
|
1087
|
+
if (document.activeElement === first) {
|
|
1088
|
+
e.preventDefault();
|
|
1089
|
+
last.focus();
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
if (document.activeElement === last) {
|
|
1093
|
+
e.preventDefault();
|
|
1094
|
+
first.focus();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
container.addEventListener("keydown", handleKeydown);
|
|
1099
|
+
return () => container.removeEventListener("keydown", handleKeydown);
|
|
1100
|
+
}
|
|
1101
|
+
var liveRegion = null;
|
|
1102
|
+
function getOrCreateLiveRegion() {
|
|
1103
|
+
if (typeof document === "undefined") {
|
|
1104
|
+
return { textContent: "" };
|
|
1105
|
+
}
|
|
1106
|
+
if (!liveRegion) {
|
|
1107
|
+
liveRegion = document.createElement("div");
|
|
1108
|
+
liveRegion.setAttribute("role", "status");
|
|
1109
|
+
liveRegion.setAttribute("aria-live", "polite");
|
|
1110
|
+
liveRegion.setAttribute("aria-atomic", "true");
|
|
1111
|
+
liveRegion.style.position = "absolute";
|
|
1112
|
+
liveRegion.style.width = "1px";
|
|
1113
|
+
liveRegion.style.height = "1px";
|
|
1114
|
+
liveRegion.style.padding = "0";
|
|
1115
|
+
liveRegion.style.margin = "-1px";
|
|
1116
|
+
liveRegion.style.overflow = "hidden";
|
|
1117
|
+
liveRegion.style.clip = "rect(0, 0, 0, 0)";
|
|
1118
|
+
liveRegion.style.whiteSpace = "nowrap";
|
|
1119
|
+
liveRegion.style.borderWidth = "0";
|
|
1120
|
+
document.body.appendChild(liveRegion);
|
|
1121
|
+
}
|
|
1122
|
+
return liveRegion;
|
|
1123
|
+
}
|
|
1124
|
+
function announce(message, priority = "polite") {
|
|
1125
|
+
const region = getOrCreateLiveRegion();
|
|
1126
|
+
region.setAttribute("aria-live", priority);
|
|
1127
|
+
region.textContent = "";
|
|
1128
|
+
requestAnimationFrame(() => {
|
|
1129
|
+
region.textContent = message;
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function cleanupLiveRegion() {
|
|
1133
|
+
if (liveRegion && typeof document !== "undefined") {
|
|
1134
|
+
liveRegion.remove();
|
|
1135
|
+
liveRegion = null;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function announceErrors(errors, opts) {
|
|
1139
|
+
const errorEntries = Object.entries(errors).filter(([_, msgs]) => msgs.length > 0);
|
|
1140
|
+
if (errorEntries.length === 0)
|
|
1141
|
+
return;
|
|
1142
|
+
const prefix = opts?.prefix ?? "Form has errors:";
|
|
1143
|
+
const messages = errorEntries.map(([field, msgs]) => `${field}: ${msgs[0]}`).join(". ");
|
|
1144
|
+
announce(`${prefix} ${messages}`, "assertive");
|
|
1145
|
+
}
|
|
1146
|
+
function announceSubmitStatus(status, opts) {
|
|
1147
|
+
switch (status) {
|
|
1148
|
+
case "submitting":
|
|
1149
|
+
announce(opts?.submittingMessage ?? "Submitting form...", "polite");
|
|
1150
|
+
break;
|
|
1151
|
+
case "success":
|
|
1152
|
+
announce(opts?.successMessage ?? "Form submitted successfully.", "polite");
|
|
1153
|
+
break;
|
|
1154
|
+
case "error":
|
|
1155
|
+
announce(opts?.errorMessage ?? "Form submission failed. Please check for errors.", "assertive");
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function prefersReducedMotion() {
|
|
1160
|
+
if (typeof window === "undefined")
|
|
1161
|
+
return false;
|
|
1162
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1163
|
+
}
|
|
1164
|
+
function isScreenReaderActive() {
|
|
1165
|
+
if (typeof window === "undefined")
|
|
1166
|
+
return false;
|
|
1167
|
+
return document.querySelector('[role="application"]') !== null || window.navigator.userAgent.includes("NVDA") || window.navigator.userAgent.includes("JAWS");
|
|
1168
|
+
}
|
|
1169
|
+
|
|
582
1170
|
// src/store.ts
|
|
583
1171
|
class FormStore {
|
|
584
1172
|
config;
|
|
585
1173
|
_signals = new Map;
|
|
586
1174
|
_errorSignals = new Map;
|
|
1175
|
+
_errorMapSignals = new Map;
|
|
587
1176
|
_formErrors;
|
|
588
1177
|
_baseline;
|
|
589
1178
|
_touchedSet = new Set;
|
|
@@ -815,6 +1404,11 @@ class FormStore {
|
|
|
815
1404
|
}
|
|
816
1405
|
setErrors(path, errors) {
|
|
817
1406
|
this.getErrors(path).set(errors);
|
|
1407
|
+
const mapSig = this._errorMapSignals.get(path);
|
|
1408
|
+
if (mapSig) {
|
|
1409
|
+
const current = mapSig.get();
|
|
1410
|
+
mapSig.set({ ...current, manual: errors });
|
|
1411
|
+
}
|
|
818
1412
|
this._updateIsValid();
|
|
819
1413
|
this._notifySubscribers();
|
|
820
1414
|
}
|
|
@@ -823,10 +1417,16 @@ class FormStore {
|
|
|
823
1417
|
const sig = this._errorSignals.get(path);
|
|
824
1418
|
if (sig)
|
|
825
1419
|
sig.set([]);
|
|
1420
|
+
const mapSig = this._errorMapSignals.get(path);
|
|
1421
|
+
if (mapSig)
|
|
1422
|
+
mapSig.set(this._emptyErrorMap());
|
|
826
1423
|
} else {
|
|
827
1424
|
for (const sig of this._errorSignals.values()) {
|
|
828
1425
|
sig.set([]);
|
|
829
1426
|
}
|
|
1427
|
+
for (const sig of this._errorMapSignals.values()) {
|
|
1428
|
+
sig.set(this._emptyErrorMap());
|
|
1429
|
+
}
|
|
830
1430
|
this._formErrors.set([]);
|
|
831
1431
|
}
|
|
832
1432
|
this._updateIsValid();
|
|
@@ -837,6 +1437,44 @@ class FormStore {
|
|
|
837
1437
|
this._updateIsValid();
|
|
838
1438
|
this._notifySubscribers();
|
|
839
1439
|
}
|
|
1440
|
+
_emptyErrorMap() {
|
|
1441
|
+
return { sync: [], async: [], schema: [], server: [], manual: [] };
|
|
1442
|
+
}
|
|
1443
|
+
getErrorMap(path) {
|
|
1444
|
+
let sig = this._errorMapSignals.get(path);
|
|
1445
|
+
if (!sig) {
|
|
1446
|
+
sig = signal2(this._emptyErrorMap());
|
|
1447
|
+
this._errorMapSignals.set(path, sig);
|
|
1448
|
+
}
|
|
1449
|
+
return sig;
|
|
1450
|
+
}
|
|
1451
|
+
setErrorsWithSource(path, errors, source) {
|
|
1452
|
+
const mapSig = this.getErrorMap(path);
|
|
1453
|
+
const current = mapSig.get();
|
|
1454
|
+
const updated = { ...current, [source]: errors };
|
|
1455
|
+
mapSig.set(updated);
|
|
1456
|
+
this._rebuildFlatErrors(path, updated);
|
|
1457
|
+
}
|
|
1458
|
+
clearErrorsBySource(path, source) {
|
|
1459
|
+
const mapSig = this._errorMapSignals.get(path);
|
|
1460
|
+
if (!mapSig)
|
|
1461
|
+
return;
|
|
1462
|
+
const current = mapSig.get();
|
|
1463
|
+
if (current[source].length === 0)
|
|
1464
|
+
return;
|
|
1465
|
+
const updated = { ...current, [source]: [] };
|
|
1466
|
+
mapSig.set(updated);
|
|
1467
|
+
this._rebuildFlatErrors(path, updated);
|
|
1468
|
+
}
|
|
1469
|
+
_rebuildFlatErrors(path, errorMap) {
|
|
1470
|
+
const flat = [];
|
|
1471
|
+
for (const source of ["sync", "async", "schema", "server", "manual"]) {
|
|
1472
|
+
flat.push(...errorMap[source]);
|
|
1473
|
+
}
|
|
1474
|
+
this.getErrors(path).set(flat);
|
|
1475
|
+
this._updateIsValid();
|
|
1476
|
+
this._notifySubscribers();
|
|
1477
|
+
}
|
|
840
1478
|
_updateIsValid() {
|
|
841
1479
|
let valid = this._formErrors.get().length === 0;
|
|
842
1480
|
if (valid) {
|
|
@@ -870,7 +1508,7 @@ class FormStore {
|
|
|
870
1508
|
if (options) {
|
|
871
1509
|
this._fieldOptions.set(path, options);
|
|
872
1510
|
if (options.validate) {
|
|
873
|
-
this._validationEngine.registerFieldValidators(path, Array.isArray(options.validate) ? options.validate : [options.validate], options.validateOn);
|
|
1511
|
+
this._validationEngine.registerFieldValidators(path, Array.isArray(options.validate) ? options.validate : [options.validate], options.validateOn, options.dependsOn);
|
|
874
1512
|
}
|
|
875
1513
|
}
|
|
876
1514
|
const value = this.getValue(path);
|
|
@@ -936,7 +1574,10 @@ class FormStore {
|
|
|
936
1574
|
if (e)
|
|
937
1575
|
e.preventDefault?.();
|
|
938
1576
|
if (!this.config.onSubmit) {
|
|
939
|
-
await this._validationEngine.validateAll();
|
|
1577
|
+
const result = await this._validationEngine.validateAll();
|
|
1578
|
+
if (!result.success && this.config.focusOnError !== false) {
|
|
1579
|
+
focusFirstError(this);
|
|
1580
|
+
}
|
|
940
1581
|
return;
|
|
941
1582
|
}
|
|
942
1583
|
await this.submitWith(this.config.onSubmit);
|
|
@@ -970,6 +1611,9 @@ class FormStore {
|
|
|
970
1611
|
this.isSubmitting.set(false);
|
|
971
1612
|
this.submitState.set("error");
|
|
972
1613
|
});
|
|
1614
|
+
if (this.config.focusOnError !== false) {
|
|
1615
|
+
focusFirstError(this);
|
|
1616
|
+
}
|
|
973
1617
|
return;
|
|
974
1618
|
}
|
|
975
1619
|
const values = this._getCurrentValues();
|
|
@@ -1003,6 +1647,32 @@ class FormStore {
|
|
|
1003
1647
|
const result = await this._validationEngine.validateAll();
|
|
1004
1648
|
return result.success;
|
|
1005
1649
|
}
|
|
1650
|
+
resetField(path) {
|
|
1651
|
+
batch(() => {
|
|
1652
|
+
const baseline = getPath(this._baseline, path);
|
|
1653
|
+
this.setValue(path, baseline);
|
|
1654
|
+
this.clearErrors(path);
|
|
1655
|
+
this.setTouched(path, false);
|
|
1656
|
+
this._dirtySet.delete(path);
|
|
1657
|
+
this.isDirty.set(this._dirtySet.size > 0);
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
async trigger(path) {
|
|
1661
|
+
if (path) {
|
|
1662
|
+
this.setTouched(path, true);
|
|
1663
|
+
const errors = await this._validationEngine.validateField(path);
|
|
1664
|
+
return errors.length === 0;
|
|
1665
|
+
}
|
|
1666
|
+
for (const p of this._fieldOptions.keys()) {
|
|
1667
|
+
this._touchedSet.add(p);
|
|
1668
|
+
}
|
|
1669
|
+
for (const p of this._validationEngine.getRegisteredPaths()) {
|
|
1670
|
+
this._touchedSet.add(p);
|
|
1671
|
+
}
|
|
1672
|
+
this._notifySubscribers();
|
|
1673
|
+
const result = await this._validationEngine.validateAll();
|
|
1674
|
+
return result.success;
|
|
1675
|
+
}
|
|
1006
1676
|
reset() {
|
|
1007
1677
|
this.resetTo(deepClone(this.config.defaultValues));
|
|
1008
1678
|
}
|
|
@@ -1112,6 +1782,7 @@ class FormStore {
|
|
|
1112
1782
|
this._watchers.clear();
|
|
1113
1783
|
this._fieldRefs.clear();
|
|
1114
1784
|
this._fieldOptions.clear();
|
|
1785
|
+
this._errorMapSignals.clear();
|
|
1115
1786
|
if (this._submitAbort) {
|
|
1116
1787
|
this._submitAbort.abort();
|
|
1117
1788
|
this._submitAbort = null;
|
|
@@ -1199,38 +1870,41 @@ function useForm(config) {
|
|
|
1199
1870
|
return storeRef.current;
|
|
1200
1871
|
}
|
|
1201
1872
|
function useField(form, name, opts) {
|
|
1873
|
+
const f = form;
|
|
1202
1874
|
const optsRef = useRef(opts);
|
|
1203
1875
|
const registered = useRef(false);
|
|
1204
1876
|
if (!registered.current || optsRef.current !== opts) {
|
|
1205
1877
|
optsRef.current = opts;
|
|
1206
1878
|
if (opts) {
|
|
1207
|
-
|
|
1879
|
+
f.register(name, opts);
|
|
1208
1880
|
}
|
|
1209
1881
|
registered.current = true;
|
|
1210
1882
|
}
|
|
1211
1883
|
useEffect(() => {
|
|
1212
1884
|
return () => {
|
|
1213
|
-
|
|
1885
|
+
f.unregister(name);
|
|
1214
1886
|
};
|
|
1215
|
-
}, [
|
|
1216
|
-
const valueSig =
|
|
1887
|
+
}, [f, name]);
|
|
1888
|
+
const valueSig = f.getSignal(name);
|
|
1217
1889
|
const value = useSignal(valueSig);
|
|
1218
|
-
const errorsSig =
|
|
1890
|
+
const errorsSig = f.getErrors(name);
|
|
1219
1891
|
const errors = useSignal(errorsSig);
|
|
1220
|
-
const touched = useSyncExternalStore(useCallback((cb) =>
|
|
1221
|
-
const dirty = useSyncExternalStore(useCallback((cb) =>
|
|
1222
|
-
const validatingSig =
|
|
1892
|
+
const touched = useSyncExternalStore(useCallback((cb) => f.subscribe(cb), [f]), () => f.getTouched(name), () => f.getTouched(name));
|
|
1893
|
+
const dirty = useSyncExternalStore(useCallback((cb) => f.subscribe(cb), [f]), () => f.getDirty(name), () => f.getDirty(name));
|
|
1894
|
+
const validatingSig = f.getFieldValidating(name);
|
|
1223
1895
|
const validating = useSignal(validatingSig);
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1227
|
-
const
|
|
1896
|
+
const errorMapSig = f.getErrorMap(name);
|
|
1897
|
+
const errorMap = useSignal(errorMapSig);
|
|
1898
|
+
const setValue = useCallback((v) => f.setValue(name, v), [f, name]);
|
|
1899
|
+
const setError = useCallback((errs) => f.setErrors(name, errs), [f, name]);
|
|
1900
|
+
const clearErrors = useCallback(() => f.clearErrors(name), [f, name]);
|
|
1901
|
+
const setTouched = useCallback((t) => f.setTouched(name, t), [f, name]);
|
|
1228
1902
|
const reset = useCallback(() => {
|
|
1229
1903
|
const baseline = getPath(form.getBaseline(), name);
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}, [form, name]);
|
|
1904
|
+
f.setValue(name, baseline);
|
|
1905
|
+
f.clearErrors(name);
|
|
1906
|
+
f.setTouched(name, false);
|
|
1907
|
+
}, [f, form, name]);
|
|
1234
1908
|
const onChange = useCallback((e) => {
|
|
1235
1909
|
let newValue;
|
|
1236
1910
|
if (opts?.parse) {
|
|
@@ -1244,12 +1918,12 @@ function useField(form, name, opts) {
|
|
|
1244
1918
|
if (opts?.transform) {
|
|
1245
1919
|
newValue = opts.transform(newValue);
|
|
1246
1920
|
}
|
|
1247
|
-
|
|
1248
|
-
}, [
|
|
1921
|
+
f.setValue(name, newValue);
|
|
1922
|
+
}, [f, name, opts]);
|
|
1249
1923
|
const onBlur = useCallback(() => {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
}, [
|
|
1924
|
+
f.setTouched(name);
|
|
1925
|
+
f.triggerBlurValidation(name);
|
|
1926
|
+
}, [f, name]);
|
|
1253
1927
|
const refCallback = useCallback((el) => {
|
|
1254
1928
|
if (form.setFieldRef) {
|
|
1255
1929
|
form.setFieldRef(name, el);
|
|
@@ -1273,6 +1947,7 @@ function useField(form, name, opts) {
|
|
|
1273
1947
|
touched,
|
|
1274
1948
|
dirty,
|
|
1275
1949
|
validating,
|
|
1950
|
+
errorMap,
|
|
1276
1951
|
setValue,
|
|
1277
1952
|
setError,
|
|
1278
1953
|
clearErrors,
|
|
@@ -1281,14 +1956,14 @@ function useField(form, name, opts) {
|
|
|
1281
1956
|
};
|
|
1282
1957
|
}
|
|
1283
1958
|
function useFieldArray(form, name) {
|
|
1284
|
-
const
|
|
1959
|
+
const idCounter2 = useRef(0);
|
|
1285
1960
|
const idsRef = useRef([]);
|
|
1286
1961
|
const arraySig = form.getSignal(name);
|
|
1287
1962
|
const rawArray = useSignal(arraySig);
|
|
1288
1963
|
const items = rawArray ?? [];
|
|
1289
1964
|
if (idsRef.current.length < items.length) {
|
|
1290
1965
|
for (let i = idsRef.current.length;i < items.length; i++) {
|
|
1291
|
-
idsRef.current.push(`${name}-${
|
|
1966
|
+
idsRef.current.push(`${name}-${idCounter2.current++}`);
|
|
1292
1967
|
}
|
|
1293
1968
|
} else if (idsRef.current.length > items.length) {
|
|
1294
1969
|
idsRef.current.length = items.length;
|
|
@@ -1300,12 +1975,12 @@ function useFieldArray(form, name) {
|
|
|
1300
1975
|
})), [items, name]);
|
|
1301
1976
|
const append = useCallback((value) => {
|
|
1302
1977
|
const current = form.getValue(name) ?? [];
|
|
1303
|
-
idsRef.current = [...idsRef.current, `${name}-${
|
|
1978
|
+
idsRef.current = [...idsRef.current, `${name}-${idCounter2.current++}`];
|
|
1304
1979
|
form.setValue(name, [...current, value]);
|
|
1305
1980
|
}, [form, name]);
|
|
1306
1981
|
const prepend = useCallback((value) => {
|
|
1307
1982
|
const current = form.getValue(name) ?? [];
|
|
1308
|
-
idsRef.current = [`${name}-${
|
|
1983
|
+
idsRef.current = [`${name}-${idCounter2.current++}`, ...idsRef.current];
|
|
1309
1984
|
form.setValue(name, [value, ...current]);
|
|
1310
1985
|
}, [form, name]);
|
|
1311
1986
|
const insert = useCallback((index, value) => {
|
|
@@ -1313,7 +1988,7 @@ function useFieldArray(form, name) {
|
|
|
1313
1988
|
const next = [...current];
|
|
1314
1989
|
next.splice(index, 0, value);
|
|
1315
1990
|
const ids = [...idsRef.current];
|
|
1316
|
-
ids.splice(index, 0, `${name}-${
|
|
1991
|
+
ids.splice(index, 0, `${name}-${idCounter2.current++}`);
|
|
1317
1992
|
idsRef.current = ids;
|
|
1318
1993
|
form.setValue(name, next);
|
|
1319
1994
|
}, [form, name]);
|
|
@@ -1353,7 +2028,7 @@ function useFieldArray(form, name) {
|
|
|
1353
2028
|
form.setValue(name, next);
|
|
1354
2029
|
}, [form, name]);
|
|
1355
2030
|
const replaceAll = useCallback((values) => {
|
|
1356
|
-
idsRef.current = values.map(() => `${name}-${
|
|
2031
|
+
idsRef.current = values.map(() => `${name}-${idCounter2.current++}`);
|
|
1357
2032
|
form.setValue(name, values);
|
|
1358
2033
|
}, [form, name]);
|
|
1359
2034
|
const clone = useCallback((index) => {
|
|
@@ -1362,7 +2037,7 @@ function useFieldArray(form, name) {
|
|
|
1362
2037
|
const cloned = deepClone(current[index]);
|
|
1363
2038
|
next.splice(index + 1, 0, cloned);
|
|
1364
2039
|
const ids = [...idsRef.current];
|
|
1365
|
-
ids.splice(index + 1, 0, `${name}-${
|
|
2040
|
+
ids.splice(index + 1, 0, `${name}-${idCounter2.current++}`);
|
|
1366
2041
|
idsRef.current = ids;
|
|
1367
2042
|
form.setValue(name, next);
|
|
1368
2043
|
}, [form, name]);
|
|
@@ -1375,225 +2050,53 @@ function useFieldArray(form, name) {
|
|
|
1375
2050
|
swap,
|
|
1376
2051
|
move,
|
|
1377
2052
|
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");
|
|
2053
|
+
replaceAll,
|
|
2054
|
+
clone
|
|
2055
|
+
};
|
|
1571
2056
|
}
|
|
1572
|
-
function
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2057
|
+
function useWatch(form, pathOrPaths) {
|
|
2058
|
+
const f = form;
|
|
2059
|
+
if (typeof pathOrPaths === "string") {
|
|
2060
|
+
const sig = f.getSignal(pathOrPaths);
|
|
2061
|
+
return useSignal(sig);
|
|
2062
|
+
}
|
|
2063
|
+
const prevRef = useRef([]);
|
|
2064
|
+
const subscribe = useCallback((cb) => {
|
|
2065
|
+
const unsubs = pathOrPaths.map((p) => f.getSignal(p).subscribe(cb));
|
|
2066
|
+
return () => unsubs.forEach((u) => u());
|
|
2067
|
+
}, [f, ...pathOrPaths]);
|
|
2068
|
+
const getSnapshot = useCallback(() => {
|
|
2069
|
+
const next = pathOrPaths.map((p) => f.getValue(p));
|
|
2070
|
+
if (next.length === prevRef.current.length && next.every((v, i) => v === prevRef.current[i])) {
|
|
2071
|
+
return prevRef.current;
|
|
2072
|
+
}
|
|
2073
|
+
prevRef.current = next;
|
|
2074
|
+
return next;
|
|
2075
|
+
}, [f, ...pathOrPaths]);
|
|
2076
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
1584
2077
|
}
|
|
1585
|
-
function
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2078
|
+
function useFormStatus(form) {
|
|
2079
|
+
const isSubmitting = useSignal(form.isSubmitting);
|
|
2080
|
+
const submitState = useSignal(form.submitState);
|
|
2081
|
+
const isValid = useSignal(form.isValid);
|
|
2082
|
+
const isDirty = useSignal(form.isDirty);
|
|
2083
|
+
const submitCount = useSignal(form.submitCount);
|
|
2084
|
+
return { isSubmitting, submitState, isValid, isDirty, submitCount };
|
|
1589
2085
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2086
|
+
// src/context.ts
|
|
2087
|
+
import { createContext, useContext, createElement } from "react";
|
|
2088
|
+
var FormContext = createContext(null);
|
|
2089
|
+
function FormProvider({
|
|
2090
|
+
form,
|
|
2091
|
+
children
|
|
2092
|
+
}) {
|
|
2093
|
+
return createElement(FormContext.Provider, { value: form }, children);
|
|
2094
|
+
}
|
|
2095
|
+
function useFormContext() {
|
|
2096
|
+
return useContext(FormContext);
|
|
1594
2097
|
}
|
|
1595
|
-
|
|
1596
2098
|
// src/components.ts
|
|
2099
|
+
import { createElement as createElement2 } from "react";
|
|
1597
2100
|
function inferInputType(name, explicitType) {
|
|
1598
2101
|
if (explicitType)
|
|
1599
2102
|
return explicitType;
|
|
@@ -1855,6 +2358,7 @@ function matches(otherField, msg) {
|
|
|
1855
2358
|
return value === other ? undefined : msg ?? `Must match ${otherField}`;
|
|
1856
2359
|
};
|
|
1857
2360
|
validator._crossField = true;
|
|
2361
|
+
validator._dependsOnField = otherField;
|
|
1858
2362
|
return validator;
|
|
1859
2363
|
}
|
|
1860
2364
|
function oneOf(values, msg) {
|
|
@@ -1954,219 +2458,6 @@ var v = {
|
|
|
1954
2458
|
compose,
|
|
1955
2459
|
when
|
|
1956
2460
|
};
|
|
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
2461
|
// src/action.ts
|
|
2171
2462
|
import { createElement as createElement3, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2, useState } from "react";
|
|
2172
2463
|
import { batch as batch2 } from "@ereo/state";
|
|
@@ -2308,7 +2599,7 @@ function ActionForm(props) {
|
|
|
2308
2599
|
if (result.errors) {
|
|
2309
2600
|
for (const [path, errors] of Object.entries(result.errors)) {
|
|
2310
2601
|
if (path) {
|
|
2311
|
-
form.
|
|
2602
|
+
form.setErrorsWithSource(path, errors, "server");
|
|
2312
2603
|
} else {
|
|
2313
2604
|
form.setFormErrors(errors);
|
|
2314
2605
|
}
|
|
@@ -2868,6 +3159,7 @@ export {
|
|
|
2868
3159
|
v,
|
|
2869
3160
|
useWizardContext,
|
|
2870
3161
|
useWizard,
|
|
3162
|
+
useWatch,
|
|
2871
3163
|
useFormStatus,
|
|
2872
3164
|
useFormContext,
|
|
2873
3165
|
useFormAction,
|
|
@@ -2876,6 +3168,7 @@ export {
|
|
|
2876
3168
|
useField,
|
|
2877
3169
|
url,
|
|
2878
3170
|
trapFocus,
|
|
3171
|
+
standardSchemaAdapter,
|
|
2879
3172
|
setPath,
|
|
2880
3173
|
required,
|
|
2881
3174
|
prefersReducedMotion,
|
|
@@ -2893,6 +3186,7 @@ export {
|
|
|
2893
3186
|
maxLength,
|
|
2894
3187
|
max,
|
|
2895
3188
|
matches,
|
|
3189
|
+
isStandardSchema,
|
|
2896
3190
|
isScreenReaderActive,
|
|
2897
3191
|
isEreoSchema,
|
|
2898
3192
|
integer,
|