@samline/forms 2.4.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -8
- package/dist/browser/global.d.ts +28 -18
- package/dist/browser/global.global.js +477 -66
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +1450 -0
- package/dist/browser/index.cjs.map +1 -0
- package/dist/browser/index.d.cts +14 -0
- package/dist/browser/index.d.ts +14 -0
- package/dist/browser/index.js +23 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chunk-WGPAVJTK.js +1415 -0
- package/dist/chunk-WGPAVJTK.js.map +1 -0
- package/dist/index.cjs +296 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -158
- package/dist/index.d.ts +4 -158
- package/dist/index.js +7 -1148
- package/dist/index.js.map +1 -1
- package/dist/registry-GzopYPCR.d.cts +164 -0
- package/dist/registry-GzopYPCR.d.ts +164 -0
- package/docs/api/add-cleanup.md +52 -0
- package/docs/api/append.md +96 -0
- package/docs/api/auto-submit.md +91 -0
- package/docs/api/clear-errors.md +69 -0
- package/docs/api/destroy.md +80 -0
- package/docs/api/disable-auto-submit.md +65 -0
- package/docs/api/element.md +78 -0
- package/docs/api/form.md +156 -0
- package/docs/api/format.md +270 -0
- package/docs/api/get-data.md +90 -0
- package/docs/api/get-field.md +87 -0
- package/docs/api/get-state.md +111 -0
- package/docs/api/get-value.md +114 -0
- package/docs/api/index.md +55 -0
- package/docs/api/observe.md +126 -0
- package/docs/api/on-submit.md +94 -0
- package/docs/api/parse-form-data.md +94 -0
- package/docs/api/prefill.md +89 -0
- package/docs/api/regex.md +155 -0
- package/docs/api/reset.md +61 -0
- package/docs/api/revalidate.md +52 -0
- package/docs/api/set-errors.md +97 -0
- package/docs/api/set-value.md +120 -0
- package/docs/api/subscribe.md +107 -0
- package/docs/api/unwatch.md +74 -0
- package/docs/api/validate-field-value.md +144 -0
- package/docs/api/validate-values.md +103 -0
- package/docs/api/validate.md +102 -0
- package/docs/api/watch.md +117 -0
- package/docs/browser.md +156 -0
- package/docs/css-styling.md +191 -0
- package/docs/getting-started.md +222 -0
- package/docs/options.md +326 -0
- package/docs/recipes.md +530 -0
- package/docs/typescript.md +615 -0
- package/docs/vanilla.md +12 -0
- package/package.json +58 -14
package/dist/index.cjs
CHANGED
|
@@ -45,7 +45,7 @@ var createAppend = (state2, helpers) => ({ tag, content, class: className, atSta
|
|
|
45
45
|
helpers.clearFieldCache();
|
|
46
46
|
if (className) {
|
|
47
47
|
const classSelector = className.trim().split(/\s+/)[0];
|
|
48
|
-
const existing = classSelector ? state2.element.
|
|
48
|
+
const existing = classSelector ? state2.element.getElementsByClassName(classSelector)[0] : null;
|
|
49
49
|
existing?.remove();
|
|
50
50
|
}
|
|
51
51
|
const node = document.createElement(tag);
|
|
@@ -59,9 +59,29 @@ var createAppend = (state2, helpers) => ({ tag, content, class: className, atSta
|
|
|
59
59
|
return node;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
// src/api/add-cleanup.ts
|
|
63
|
+
var createAddCleanup = (state2, _helpers) => (cleanup) => {
|
|
64
|
+
if (state2.isDestroyed) {
|
|
65
|
+
cleanup();
|
|
66
|
+
return () => void 0;
|
|
67
|
+
}
|
|
68
|
+
const registration = () => cleanup();
|
|
69
|
+
state2.cleanups.add(registration);
|
|
70
|
+
let active = true;
|
|
71
|
+
return () => {
|
|
72
|
+
if (!active) return;
|
|
73
|
+
active = false;
|
|
74
|
+
state2.cleanups.delete(registration);
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
62
78
|
// src/api/auto-submit.ts
|
|
63
79
|
var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
64
80
|
state2.autoSubmitEnabled = next !== false;
|
|
81
|
+
if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
|
|
82
|
+
clearTimeout(state2.autoSubmitTimer);
|
|
83
|
+
state2.autoSubmitTimer = null;
|
|
84
|
+
}
|
|
65
85
|
state2.autoSubmitDebounce = typeof next === "object" && next ? next.debounce ?? 0 : 0;
|
|
66
86
|
helpers.notifySubscribers();
|
|
67
87
|
return state2.api;
|
|
@@ -298,6 +318,12 @@ var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
|
|
|
298
318
|
};
|
|
299
319
|
var applyFormat = async (state2, helpers, config) => {
|
|
300
320
|
if (!state2.element || !state2.api) return;
|
|
321
|
+
if (Array.isArray(config.field) && config.displayField) {
|
|
322
|
+
console.error(
|
|
323
|
+
"[samline/forms] `displayField` can only be used when `field` is a string."
|
|
324
|
+
);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
301
327
|
const formatType = config.type;
|
|
302
328
|
const formatOptions = config.options;
|
|
303
329
|
const fieldNames = resolveFieldNames(config);
|
|
@@ -335,6 +361,8 @@ var applyFormat = async (state2, helpers, config) => {
|
|
|
335
361
|
visible,
|
|
336
362
|
mirror,
|
|
337
363
|
mirrorIsOwned,
|
|
364
|
+
defaultVisible: null,
|
|
365
|
+
defaultRaw: null,
|
|
338
366
|
handler: UNBOUND
|
|
339
367
|
};
|
|
340
368
|
bucket.set(fieldName, entry);
|
|
@@ -347,6 +375,10 @@ var applyFormat = async (state2, helpers, config) => {
|
|
|
347
375
|
}
|
|
348
376
|
}
|
|
349
377
|
const formatter = await loadFormatter();
|
|
378
|
+
if (state2.isDestroyed) {
|
|
379
|
+
rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
350
382
|
if (!formatter) {
|
|
351
383
|
rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
|
|
352
384
|
return;
|
|
@@ -377,6 +409,16 @@ var applyFormat = async (state2, helpers, config) => {
|
|
|
377
409
|
} else if (entry.mirrorIsOwned && entry.mirror.value !== "") {
|
|
378
410
|
entry.mirror.value = "";
|
|
379
411
|
}
|
|
412
|
+
entry.defaultVisible = entry.visible.value;
|
|
413
|
+
entry.defaultRaw = entry.mirror.value;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var resetFormattedFields = (state2) => {
|
|
417
|
+
const bucket = registry.get(state2);
|
|
418
|
+
if (!bucket) return;
|
|
419
|
+
for (const entry of bucket.values()) {
|
|
420
|
+
if (entry.defaultVisible !== null) entry.visible.value = entry.defaultVisible;
|
|
421
|
+
if (entry.defaultRaw !== null) entry.mirror.value = entry.defaultRaw;
|
|
380
422
|
}
|
|
381
423
|
};
|
|
382
424
|
var rollbackPhase1 = (entries, bucket, registry2) => {
|
|
@@ -460,12 +502,24 @@ var createDestroy = (state2, _helpers) => () => {
|
|
|
460
502
|
}
|
|
461
503
|
state2.mutationObserver?.disconnect();
|
|
462
504
|
state2.mutationObserver = null;
|
|
505
|
+
const cleanups = Array.from(state2.cleanups).reverse();
|
|
506
|
+
state2.cleanups.clear();
|
|
507
|
+
for (const cleanup of cleanups) {
|
|
508
|
+
try {
|
|
509
|
+
cleanup();
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.error("[forms] cleanup failed", error);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
463
514
|
state2.watchedFields.clear();
|
|
464
515
|
state2.subscribers.clear();
|
|
465
516
|
state2.submitHandlers.clear();
|
|
466
517
|
state2.fieldCache.clear();
|
|
467
518
|
state2.manualErrors = {};
|
|
468
519
|
state2.validationErrors = {};
|
|
520
|
+
state2.validationGroupErrors = {};
|
|
521
|
+
state2.validationElementErrors.clear();
|
|
522
|
+
state2.pendingSubmitCount = 0;
|
|
469
523
|
cleanupFormatRegistry(state2);
|
|
470
524
|
};
|
|
471
525
|
|
|
@@ -483,19 +537,24 @@ var createDisableAutoSubmit = (state2, helpers) => () => {
|
|
|
483
537
|
// src/core/serialize.ts
|
|
484
538
|
var isEmptyFile = (value) => value instanceof File && value.size === 0 && value.name === "";
|
|
485
539
|
var appendValue = (data, key, value) => {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
540
|
+
if (!Object.prototype.hasOwnProperty.call(data, key)) {
|
|
541
|
+
Object.defineProperty(data, key, {
|
|
542
|
+
value,
|
|
543
|
+
writable: true,
|
|
544
|
+
enumerable: true,
|
|
545
|
+
configurable: true
|
|
546
|
+
});
|
|
489
547
|
return;
|
|
490
548
|
}
|
|
549
|
+
const current = data[key];
|
|
491
550
|
if (Array.isArray(current)) {
|
|
492
551
|
current.push(value);
|
|
493
552
|
return;
|
|
494
553
|
}
|
|
495
554
|
data[key] = [current, value];
|
|
496
555
|
};
|
|
497
|
-
var parseFormData = (formElement) => {
|
|
498
|
-
const raw = new FormData(formElement);
|
|
556
|
+
var parseFormData = (formElement, submitter) => {
|
|
557
|
+
const raw = submitter ? new FormData(formElement, submitter) : new FormData(formElement);
|
|
499
558
|
const formData = new FormData();
|
|
500
559
|
const data = {};
|
|
501
560
|
raw.forEach((value, key) => {
|
|
@@ -527,6 +586,7 @@ var createEmptyFormState = () => ({
|
|
|
527
586
|
isValid: true,
|
|
528
587
|
isValidated: false,
|
|
529
588
|
autoSubmit: false,
|
|
589
|
+
isSubmitting: false,
|
|
530
590
|
submitCount: 0
|
|
531
591
|
});
|
|
532
592
|
var cloneErrors = (errors) => Object.fromEntries(
|
|
@@ -558,6 +618,7 @@ var createGetState = (state2, helpers) => () => {
|
|
|
558
618
|
isValid: Object.keys(errors).length === 0,
|
|
559
619
|
isValidated: state2.isValidated,
|
|
560
620
|
autoSubmit: state2.autoSubmitEnabled,
|
|
621
|
+
isSubmitting: state2.pendingSubmitCount > 0,
|
|
561
622
|
submitCount: state2.submitCount
|
|
562
623
|
};
|
|
563
624
|
};
|
|
@@ -577,8 +638,8 @@ var resolveFormElement = (target) => {
|
|
|
577
638
|
}
|
|
578
639
|
return null;
|
|
579
640
|
};
|
|
580
|
-
var getNamedFields = (form2) => Array.from(form2.
|
|
581
|
-
isFormFieldElement
|
|
641
|
+
var getNamedFields = (form2) => Array.from(form2.elements).filter(
|
|
642
|
+
(element) => isFormFieldElement(element) && Boolean(element.name)
|
|
582
643
|
);
|
|
583
644
|
var queryNamedFields = (form2, name) => getNamedFields(form2).filter((field) => field.name === name);
|
|
584
645
|
var isFieldElement = (value) => isFormFieldElement(value);
|
|
@@ -625,6 +686,9 @@ var readFieldValue = (fields) => {
|
|
|
625
686
|
}
|
|
626
687
|
return fields.map((f) => f.value);
|
|
627
688
|
}
|
|
689
|
+
if (first instanceof HTMLSelectElement && first.multiple) {
|
|
690
|
+
return Array.from(first.selectedOptions, (option) => option.value);
|
|
691
|
+
}
|
|
628
692
|
if (first instanceof HTMLSelectElement || first instanceof HTMLTextAreaElement) {
|
|
629
693
|
return first.value;
|
|
630
694
|
}
|
|
@@ -674,6 +738,13 @@ var writeFieldValue = (fields, value) => {
|
|
|
674
738
|
field.value = normalizedValue;
|
|
675
739
|
continue;
|
|
676
740
|
}
|
|
741
|
+
if (field instanceof HTMLSelectElement && field.multiple) {
|
|
742
|
+
const selectedValues = normalizedArray ?? [normalizedValue];
|
|
743
|
+
for (const option of field.options) {
|
|
744
|
+
option.selected = selectedValues.includes(option.value);
|
|
745
|
+
}
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
677
748
|
if (distributeArray && normalizedArray) {
|
|
678
749
|
field.value = normalizedArray[index] ?? "";
|
|
679
750
|
continue;
|
|
@@ -683,7 +754,7 @@ var writeFieldValue = (fields, value) => {
|
|
|
683
754
|
};
|
|
684
755
|
var clearAttributes = (form2, attributes) => {
|
|
685
756
|
for (const attribute of attributes) {
|
|
686
|
-
form2
|
|
757
|
+
getNamedFields(form2).forEach((field) => field.removeAttribute(attribute));
|
|
687
758
|
}
|
|
688
759
|
};
|
|
689
760
|
var submitForm = (form2) => {
|
|
@@ -742,10 +813,17 @@ var createPrefill = (state2, _helpers) => (fieldName) => {
|
|
|
742
813
|
var createReset = (state2, helpers) => () => {
|
|
743
814
|
if (!state2.element) return state2.api;
|
|
744
815
|
state2.element.reset();
|
|
816
|
+
resetFormattedFields(state2);
|
|
745
817
|
state2.manualErrors = {};
|
|
746
818
|
state2.validationErrors = {};
|
|
747
|
-
|
|
748
|
-
|
|
819
|
+
state2.validationGroupErrors = {};
|
|
820
|
+
state2.validationElementErrors.clear();
|
|
821
|
+
clearAttributes(state2.element, [
|
|
822
|
+
state2.attributes.error,
|
|
823
|
+
state2.attributes.filled,
|
|
824
|
+
"aria-invalid"
|
|
825
|
+
]);
|
|
826
|
+
helpers.syncVisualState();
|
|
749
827
|
helpers.notifySubscribers();
|
|
750
828
|
return state2.api;
|
|
751
829
|
};
|
|
@@ -785,8 +863,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
|
|
|
785
863
|
writeFieldValue(fields, value);
|
|
786
864
|
const firstField = fields[0];
|
|
787
865
|
if (!firstField) return state2.api;
|
|
788
|
-
|
|
789
|
-
firstField.dispatchEvent(new Event(eventType, { bubbles: true }));
|
|
866
|
+
firstField.dispatchEvent(new Event("input", { bubbles: true }));
|
|
790
867
|
return state2.api;
|
|
791
868
|
};
|
|
792
869
|
|
|
@@ -851,13 +928,35 @@ var toPatternTarget = (value) => {
|
|
|
851
928
|
}
|
|
852
929
|
return "";
|
|
853
930
|
};
|
|
854
|
-
var
|
|
931
|
+
var valuesAreEqual = (left, right) => {
|
|
932
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
933
|
+
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
934
|
+
}
|
|
935
|
+
return left === right;
|
|
936
|
+
};
|
|
937
|
+
var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
938
|
+
var parseStrictNumber = (value) => {
|
|
939
|
+
if (typeof value !== "string") return void 0;
|
|
940
|
+
const candidate = value.trim();
|
|
941
|
+
if (!DECIMAL_PATTERN.test(candidate)) return void 0;
|
|
942
|
+
const number = Number(candidate);
|
|
943
|
+
return Number.isFinite(number) ? number : void 0;
|
|
944
|
+
};
|
|
945
|
+
var validateValue = (field, value, rules, values, contextOverrides = {}) => {
|
|
855
946
|
const errors = [];
|
|
856
|
-
const context = {
|
|
947
|
+
const context = {
|
|
948
|
+
field,
|
|
949
|
+
value,
|
|
950
|
+
values,
|
|
951
|
+
...contextOverrides
|
|
952
|
+
};
|
|
857
953
|
const required = resolveRule(rules.required);
|
|
858
954
|
const minLength = resolveRule(rules.minLength);
|
|
859
955
|
const maxLength = resolveRule(rules.maxLength);
|
|
860
956
|
const pattern = resolveRule(rules.pattern);
|
|
957
|
+
const numeric = resolveRule(rules.numeric);
|
|
958
|
+
const min = resolveRule(rules.min);
|
|
959
|
+
const max = resolveRule(rules.max);
|
|
861
960
|
if (required.value && !hasValue(value)) {
|
|
862
961
|
errors.push(required.message ?? "This field is required.");
|
|
863
962
|
}
|
|
@@ -867,8 +966,26 @@ var validateFieldValue = (field, value, rules, values) => {
|
|
|
867
966
|
if (maxLength.value !== void 0 && getValueLength(value) > maxLength.value) {
|
|
868
967
|
errors.push(maxLength.message ?? `Maximum length is ${maxLength.value}.`);
|
|
869
968
|
}
|
|
870
|
-
if (pattern.value && hasValue(value)
|
|
871
|
-
|
|
969
|
+
if (pattern.value && hasValue(value)) {
|
|
970
|
+
pattern.value.lastIndex = 0;
|
|
971
|
+
const matches = pattern.value.test(toPatternTarget(value));
|
|
972
|
+
pattern.value.lastIndex = 0;
|
|
973
|
+
if (!matches) {
|
|
974
|
+
errors.push(pattern.message ?? "Value does not match the required pattern.");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (hasValue(value) && (numeric.value || min.value !== void 0 || max.value !== void 0)) {
|
|
978
|
+
const number = parseStrictNumber(value);
|
|
979
|
+
if (number === void 0) {
|
|
980
|
+
errors.push(numeric.message ?? "Value must be a number.");
|
|
981
|
+
} else {
|
|
982
|
+
if (min.value !== void 0 && number < min.value) {
|
|
983
|
+
errors.push(min.message ?? `Minimum value is ${min.value}.`);
|
|
984
|
+
}
|
|
985
|
+
if (max.value !== void 0 && number > max.value) {
|
|
986
|
+
errors.push(max.message ?? `Maximum value is ${max.value}.`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
872
989
|
}
|
|
873
990
|
const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
|
|
874
991
|
for (const validator of custom) {
|
|
@@ -878,6 +995,41 @@ var validateFieldValue = (field, value, rules, values) => {
|
|
|
878
995
|
}
|
|
879
996
|
return errors;
|
|
880
997
|
};
|
|
998
|
+
var validateFieldValueDetailed = (field, value, rules, values, items) => {
|
|
999
|
+
const groupErrors = validateValue(field, value, rules, values);
|
|
1000
|
+
const sameAs = resolveRule(rules.sameAs);
|
|
1001
|
+
if (sameAs.value !== void 0) {
|
|
1002
|
+
const otherValue = values[sameAs.value];
|
|
1003
|
+
if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
|
|
1004
|
+
groupErrors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const itemErrors = [];
|
|
1008
|
+
if (rules.each) {
|
|
1009
|
+
const sourceItems = items ?? (Array.isArray(value) ? value.map((entry) => ({
|
|
1010
|
+
value: typeof entry === "string" ? entry : [entry]
|
|
1011
|
+
})) : [{ value }]);
|
|
1012
|
+
sourceItems.forEach((item, index) => {
|
|
1013
|
+
const overrides = {
|
|
1014
|
+
index
|
|
1015
|
+
};
|
|
1016
|
+
if (item.element) overrides.element = item.element;
|
|
1017
|
+
const messages = validateValue(field, item.value, rules.each, values, overrides);
|
|
1018
|
+
if (messages.length === 0) return;
|
|
1019
|
+
const error = {
|
|
1020
|
+
index,
|
|
1021
|
+
messages
|
|
1022
|
+
};
|
|
1023
|
+
if (item.element) error.element = item.element;
|
|
1024
|
+
itemErrors.push(error);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return { groupErrors, itemErrors };
|
|
1028
|
+
};
|
|
1029
|
+
var validateFieldValue = (field, value, rules, values) => {
|
|
1030
|
+
const result = validateFieldValueDetailed(field, value, rules, values);
|
|
1031
|
+
return [...result.groupErrors, ...result.itemErrors.flatMap((item) => item.messages)];
|
|
1032
|
+
};
|
|
881
1033
|
var validateValues = (values, schema) => {
|
|
882
1034
|
const errors = {};
|
|
883
1035
|
for (const [field, rules] of Object.entries(schema)) {
|
|
@@ -919,17 +1071,36 @@ var createFormController = (target, options = {}) => {
|
|
|
919
1071
|
fieldCache: /* @__PURE__ */ new Map(),
|
|
920
1072
|
manualErrors: {},
|
|
921
1073
|
validationErrors: {},
|
|
1074
|
+
validationGroupErrors: {},
|
|
1075
|
+
validationElementErrors: /* @__PURE__ */ new Map(),
|
|
922
1076
|
isValidated: Boolean(normalizedOptions.autoValidate),
|
|
923
1077
|
autoSubmitEnabled: false,
|
|
924
1078
|
autoSubmitDebounce: 0,
|
|
925
1079
|
submitCount: 0,
|
|
1080
|
+
pendingSubmitCount: 0,
|
|
926
1081
|
autoSubmitTimer: null,
|
|
927
1082
|
isDestroyed: false,
|
|
928
1083
|
listeners: [],
|
|
929
1084
|
mutationObserver: null,
|
|
1085
|
+
cleanups: /* @__PURE__ */ new Set(),
|
|
930
1086
|
formattedFields: /* @__PURE__ */ new Map(),
|
|
931
1087
|
api: null
|
|
932
1088
|
};
|
|
1089
|
+
const validationDependents = /* @__PURE__ */ new Map();
|
|
1090
|
+
for (const [field, rules] of Object.entries(state2.validators)) {
|
|
1091
|
+
const sources = /* @__PURE__ */ new Set();
|
|
1092
|
+
const sameAs = rules.sameAs;
|
|
1093
|
+
if (sameAs !== void 0) {
|
|
1094
|
+
sources.add(typeof sameAs === "string" ? sameAs : sameAs.value);
|
|
1095
|
+
}
|
|
1096
|
+
if (typeof rules.dependsOn === "string") sources.add(rules.dependsOn);
|
|
1097
|
+
else rules.dependsOn?.forEach((source) => sources.add(source));
|
|
1098
|
+
for (const source of sources) {
|
|
1099
|
+
const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
|
|
1100
|
+
dependents.add(field);
|
|
1101
|
+
validationDependents.set(source, dependents);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
933
1104
|
const notifySubscribers = () => {
|
|
934
1105
|
if (!state2.api) return;
|
|
935
1106
|
const snapshot = state2.api.getState();
|
|
@@ -948,10 +1119,9 @@ var createFormController = (target, options = {}) => {
|
|
|
948
1119
|
const clearFieldCache = () => state2.fieldCache.clear();
|
|
949
1120
|
const getFieldsByName = (name) => {
|
|
950
1121
|
if (!state2.element) return [];
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
return state2.fieldCache.get(name) ?? [];
|
|
1122
|
+
const fields = queryNamedFields(state2.element, name);
|
|
1123
|
+
state2.fieldCache.set(name, fields);
|
|
1124
|
+
return fields;
|
|
955
1125
|
};
|
|
956
1126
|
const getTrackedFieldNames = () => {
|
|
957
1127
|
if (!state2.element) return Object.keys(state2.validators);
|
|
@@ -978,16 +1148,21 @@ var createFormController = (target, options = {}) => {
|
|
|
978
1148
|
const syncVisualState = (names) => {
|
|
979
1149
|
if (!state2.element) return;
|
|
980
1150
|
const sourceNames = names ?? getTrackedFieldNames();
|
|
981
|
-
const errors = getMergedErrors();
|
|
982
1151
|
for (const name of sourceNames) {
|
|
983
1152
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
984
1153
|
const canonical = resolveCanonicalForName(state2, name) ?? name;
|
|
985
1154
|
const fields = getFieldsByName(displayName);
|
|
986
|
-
const
|
|
987
|
-
|
|
1155
|
+
const hasGroupError = Boolean(
|
|
1156
|
+
state2.validationGroupErrors[canonical]?.length || state2.manualErrors[canonical]?.length
|
|
1157
|
+
);
|
|
1158
|
+
const elementErrors = state2.validationElementErrors.get(canonical);
|
|
1159
|
+
fields.forEach((field) => {
|
|
1160
|
+
const hasError = hasGroupError || Boolean(elementErrors?.get(field)?.length);
|
|
988
1161
|
applyBooleanAttribute(field, attributes.filled, isFieldFilled(field));
|
|
989
1162
|
applyBooleanAttribute(field, attributes.error, hasError);
|
|
990
|
-
|
|
1163
|
+
if (hasError) field.setAttribute("aria-invalid", "true");
|
|
1164
|
+
else field.removeAttribute("aria-invalid");
|
|
1165
|
+
});
|
|
991
1166
|
}
|
|
992
1167
|
};
|
|
993
1168
|
void readFieldValue;
|
|
@@ -996,23 +1171,59 @@ var createFormController = (target, options = {}) => {
|
|
|
996
1171
|
const targetNames = names ?? Object.keys(state2.validators);
|
|
997
1172
|
const values = getValues();
|
|
998
1173
|
const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
|
|
1174
|
+
const nextGroupErrors = names ? cloneErrors(state2.validationGroupErrors) : {};
|
|
1175
|
+
const nextElementErrors = names ? new Map(state2.validationElementErrors) : /* @__PURE__ */ new Map();
|
|
999
1176
|
for (const name of targetNames) {
|
|
1000
1177
|
const rules = state2.validators[name];
|
|
1001
1178
|
if (!rules) {
|
|
1002
1179
|
delete nextValidationErrors[name];
|
|
1180
|
+
delete nextGroupErrors[name];
|
|
1181
|
+
nextElementErrors.delete(name);
|
|
1003
1182
|
continue;
|
|
1004
1183
|
}
|
|
1005
|
-
const
|
|
1184
|
+
const fields = getFieldsByName(name);
|
|
1185
|
+
const displayFields = getFieldsByName(resolveDisplayNameForName(state2, name));
|
|
1186
|
+
const items = fields.map((field, index) => ({
|
|
1187
|
+
value: readFieldValue([field]),
|
|
1188
|
+
element: displayFields[index] ?? field
|
|
1189
|
+
}));
|
|
1190
|
+
const result = validateFieldValueDetailed(name, values[name], rules, values, items);
|
|
1191
|
+
const messages = [...result.groupErrors, ...result.itemErrors.flatMap((item) => item.messages)];
|
|
1006
1192
|
if (messages.length > 0) nextValidationErrors[name] = messages;
|
|
1007
1193
|
else delete nextValidationErrors[name];
|
|
1194
|
+
if (result.groupErrors.length > 0) nextGroupErrors[name] = result.groupErrors;
|
|
1195
|
+
else delete nextGroupErrors[name];
|
|
1196
|
+
const perElement = /* @__PURE__ */ new Map();
|
|
1197
|
+
result.itemErrors.forEach((item) => {
|
|
1198
|
+
if (item.element) perElement.set(item.element, item.messages);
|
|
1199
|
+
});
|
|
1200
|
+
if (perElement.size > 0) nextElementErrors.set(name, perElement);
|
|
1201
|
+
else nextElementErrors.delete(name);
|
|
1008
1202
|
}
|
|
1009
1203
|
state2.validationErrors = nextValidationErrors;
|
|
1204
|
+
state2.validationGroupErrors = nextGroupErrors;
|
|
1205
|
+
state2.validationElementErrors = nextElementErrors;
|
|
1010
1206
|
syncVisualState(names);
|
|
1011
1207
|
return {
|
|
1012
1208
|
isValid: Object.keys(getMergedErrors()).length === 0,
|
|
1013
1209
|
errors: cloneErrors(getMergedErrors())
|
|
1014
1210
|
};
|
|
1015
1211
|
};
|
|
1212
|
+
const getAffectedValidationNames = (name) => {
|
|
1213
|
+
const affected = [];
|
|
1214
|
+
const pending = [name];
|
|
1215
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1216
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
1217
|
+
const current = pending[index];
|
|
1218
|
+
if (visited.has(current)) continue;
|
|
1219
|
+
visited.add(current);
|
|
1220
|
+
affected.push(current);
|
|
1221
|
+
for (const dependent of validationDependents.get(current) ?? []) {
|
|
1222
|
+
if (!visited.has(dependent)) pending.push(dependent);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return affected;
|
|
1226
|
+
};
|
|
1016
1227
|
const emitFieldWatchers = (name) => {
|
|
1017
1228
|
if (!state2.element || !state2.api) return;
|
|
1018
1229
|
const callbacks = state2.watchedFields.get(name);
|
|
@@ -1058,14 +1269,21 @@ var createFormController = (target, options = {}) => {
|
|
|
1058
1269
|
delete state2.manualErrors[canonical];
|
|
1059
1270
|
}
|
|
1060
1271
|
syncVisualState([displayName]);
|
|
1061
|
-
|
|
1062
|
-
|
|
1272
|
+
const affectedValidationNames = getAffectedValidationNames(canonical);
|
|
1273
|
+
if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
|
|
1274
|
+
validateNames(affectedValidationNames);
|
|
1063
1275
|
}
|
|
1064
1276
|
emitFieldWatchers(canonical);
|
|
1065
1277
|
if (canonical !== name) emitFieldWatchers(name);
|
|
1066
1278
|
notifySubscribers();
|
|
1067
1279
|
scheduleAutoSubmit();
|
|
1068
1280
|
};
|
|
1281
|
+
const handleExternalDelegatedEvent = (event) => {
|
|
1282
|
+
const target2 = event.target;
|
|
1283
|
+
if (!(target2 instanceof Element) || !isFieldElement(target2)) return;
|
|
1284
|
+
if (target2.form !== state2.element || state2.element?.contains(target2)) return;
|
|
1285
|
+
handleDelegatedEvent(event);
|
|
1286
|
+
};
|
|
1069
1287
|
const handleSubmitEvent = (event) => {
|
|
1070
1288
|
if (!state2.element || state2.isDestroyed || !state2.api) return;
|
|
1071
1289
|
if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
|
|
@@ -1075,21 +1293,59 @@ var createFormController = (target, options = {}) => {
|
|
|
1075
1293
|
if (shouldPrevent) event.preventDefault();
|
|
1076
1294
|
state2.submitCount += 1;
|
|
1077
1295
|
notifySubscribers();
|
|
1078
|
-
if (!validation.isValid)
|
|
1079
|
-
|
|
1296
|
+
if (!validation.isValid) {
|
|
1297
|
+
for (const name of Object.keys(validation.errors)) {
|
|
1298
|
+
const displayName = resolveDisplayNameForName(state2, name);
|
|
1299
|
+
const fields = getFieldsByName(displayName);
|
|
1300
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
1301
|
+
const field = fields.find((candidate2) => {
|
|
1302
|
+
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1303
|
+
if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
|
|
1304
|
+
return elementErrors?.has(candidate2);
|
|
1305
|
+
}) ?? fields.find((candidate2) => {
|
|
1306
|
+
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1307
|
+
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
1308
|
+
});
|
|
1309
|
+
if (field) {
|
|
1310
|
+
field.focus();
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const candidate = event;
|
|
1317
|
+
const submitter = candidate.submitter instanceof HTMLElement ? candidate.submitter : null;
|
|
1318
|
+
const { data, formData } = parseFormData(state2.element, submitter);
|
|
1080
1319
|
const snapshot = state2.api.getState();
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1320
|
+
const pending = [];
|
|
1321
|
+
let synchronousError;
|
|
1322
|
+
let hasSynchronousError = false;
|
|
1323
|
+
try {
|
|
1324
|
+
for (const handler of handlers) {
|
|
1325
|
+
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
1326
|
+
if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
|
|
1327
|
+
}
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
synchronousError = error;
|
|
1330
|
+
hasSynchronousError = true;
|
|
1331
|
+
}
|
|
1332
|
+
if (pending.length > 0) {
|
|
1333
|
+
state2.pendingSubmitCount += 1;
|
|
1334
|
+
notifySubscribers();
|
|
1335
|
+
void Promise.allSettled(pending).then(() => {
|
|
1336
|
+
state2.pendingSubmitCount = Math.max(0, state2.pendingSubmitCount - 1);
|
|
1337
|
+
if (!state2.isDestroyed) notifySubscribers();
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
if (hasSynchronousError) throw synchronousError;
|
|
1084
1341
|
};
|
|
1085
1342
|
const startMutationObserver = () => {
|
|
1086
1343
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
1087
1344
|
state2.mutationObserver = new MutationObserver(() => {
|
|
1088
1345
|
clearFieldCache();
|
|
1089
1346
|
if (state2.isValidated) {
|
|
1090
|
-
syncVisualState();
|
|
1091
1347
|
validateNames();
|
|
1092
|
-
}
|
|
1348
|
+
} else syncVisualState();
|
|
1093
1349
|
notifySubscribers();
|
|
1094
1350
|
});
|
|
1095
1351
|
state2.mutationObserver.observe(state2.element, {
|
|
@@ -1110,6 +1366,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1110
1366
|
return state2.options;
|
|
1111
1367
|
},
|
|
1112
1368
|
onSubmit: createOnSubmit(state2, helpers),
|
|
1369
|
+
addCleanup: createAddCleanup(state2, helpers),
|
|
1113
1370
|
watch: createWatch(state2, helpers),
|
|
1114
1371
|
observe: createObserve(state2, helpers),
|
|
1115
1372
|
unwatch: createUnwatch(state2, helpers),
|
|
@@ -1135,7 +1392,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1135
1392
|
state2.api = api;
|
|
1136
1393
|
if (state2.element) {
|
|
1137
1394
|
addListener2(state2.element, "input", handleDelegatedEvent);
|
|
1138
|
-
addListener2(
|
|
1395
|
+
addListener2(document, "input", handleExternalDelegatedEvent);
|
|
1139
1396
|
addListener2(state2.element, "submit", handleSubmitEvent);
|
|
1140
1397
|
startMutationObserver();
|
|
1141
1398
|
}
|
|
@@ -1147,10 +1404,8 @@ var createFormController = (target, options = {}) => {
|
|
|
1147
1404
|
api.format(config);
|
|
1148
1405
|
}
|
|
1149
1406
|
}
|
|
1150
|
-
if (state2.isValidated)
|
|
1151
|
-
|
|
1152
|
-
validateNames();
|
|
1153
|
-
}
|
|
1407
|
+
if (state2.isValidated) validateNames();
|
|
1408
|
+
else syncVisualState();
|
|
1154
1409
|
return api;
|
|
1155
1410
|
};
|
|
1156
1411
|
|
|
@@ -1165,6 +1420,7 @@ var newForm = (input) => {
|
|
|
1165
1420
|
console.error("Form ID is required");
|
|
1166
1421
|
return;
|
|
1167
1422
|
}
|
|
1423
|
+
available[id]?.destroy();
|
|
1168
1424
|
const controller = form(id, { ...options });
|
|
1169
1425
|
available[id] = controller;
|
|
1170
1426
|
return controller;
|