@samline/forms 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/browser/global.d.ts +19 -3
- package/dist/browser/global.global.js +176 -30
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +176 -30
- package/dist/browser/index.cjs.map +1 -1
- package/dist/browser/index.d.cts +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/{chunk-L4GRKSQ5.js → chunk-MMHX34WB.js} +181 -31
- package/dist/chunk-MMHX34WB.js.map +1 -0
- package/dist/index.cjs +180 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{registry-BpqJetAt.d.cts → registry-GzopYPCR.d.cts} +20 -4
- package/dist/{registry-BpqJetAt.d.ts → registry-GzopYPCR.d.ts} +20 -4
- package/docs/api/add-cleanup.md +52 -0
- package/docs/api/destroy.md +16 -1
- package/docs/api/form.md +4 -2
- package/docs/api/format.md +3 -3
- package/docs/api/get-state.md +17 -1
- package/docs/api/index.md +1 -0
- package/docs/api/on-submit.md +7 -3
- package/docs/api/reset.md +3 -2
- package/docs/api/revalidate.md +3 -1
- package/docs/api/subscribe.md +16 -0
- package/docs/api/validate-field-value.md +39 -3
- package/docs/api/validate-values.md +4 -1
- package/docs/api/validate.md +5 -3
- package/docs/browser.md +5 -5
- package/docs/css-styling.md +5 -1
- package/docs/getting-started.md +13 -8
- package/docs/options.md +69 -2
- package/docs/recipes.md +65 -11
- package/docs/typescript.md +47 -8
- package/package.json +1 -1
- package/dist/chunk-L4GRKSQ5.js.map +0 -1
package/README.md
CHANGED
|
@@ -42,10 +42,10 @@ Requires Node 20+ when bundling. Runtime target is ES2020.
|
|
|
42
42
|
Use the browser build when you do not have a bundler and need to run the package directly in HTML, Shopify, WordPress, or any traditional template.
|
|
43
43
|
|
|
44
44
|
```html
|
|
45
|
-
<script src="https://unpkg.com/@samline/forms@2.
|
|
45
|
+
<script src="https://unpkg.com/@samline/forms@2.7.1/dist/browser/global.global.js"></script>
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
> Pin the version in production. Replace `2.
|
|
48
|
+
> Pin the version in production. Replace `2.7.1` with the version you ship.
|
|
49
49
|
|
|
50
50
|
The browser bundle exposes a single global: `window.Forms`.
|
|
51
51
|
|
|
@@ -55,7 +55,7 @@ The browser bundle exposes a single global: `window.Forms`.
|
|
|
55
55
|
<button type="submit">Send</button>
|
|
56
56
|
</form>
|
|
57
57
|
|
|
58
|
-
<script src="https://unpkg.com/@samline/forms@2.
|
|
58
|
+
<script src="https://unpkg.com/@samline/forms@2.7.1/dist/browser/global.global.js"></script>
|
|
59
59
|
<script>
|
|
60
60
|
const contactForm = window.Forms.newForm({ id: 'contact-form' })
|
|
61
61
|
|
|
@@ -134,7 +134,7 @@ The controller is built around one factory and a small set of focused methods. M
|
|
|
134
134
|
|
|
135
135
|
| Group | Methods |
|
|
136
136
|
| --- | --- |
|
|
137
|
-
| Lifecycle | [`form`](docs/api/form.md) · [`destroy`](docs/api/destroy.md) · [`reset`](docs/api/reset.md) |
|
|
137
|
+
| Lifecycle | [`form`](docs/api/form.md) · [`addCleanup`](docs/api/add-cleanup.md) · [`destroy`](docs/api/destroy.md) · [`reset`](docs/api/reset.md) |
|
|
138
138
|
| Registry (vanilla) | [`browser`](docs/getting-started.md#browser-registry-helpers) — bundler-friendly `{ form, newForm, destroyForm, available }` singleton. |
|
|
139
139
|
| Properties | [`element`](docs/api/element.md) · [`options`](docs/options.md) |
|
|
140
140
|
| Submission | [`onSubmit`](docs/api/on-submit.md) · [`autoSubmit`](docs/api/auto-submit.md) · [`disableAutoSubmit`](docs/api/disable-auto-submit.md) |
|
|
@@ -144,6 +144,8 @@ The controller is built around one factory and a small set of focused methods. M
|
|
|
144
144
|
| State and data | [`getData`](docs/api/get-data.md) · [`getState`](docs/api/get-state.md) · [`append`](docs/api/append.md) |
|
|
145
145
|
| Pure helpers | [`parseFormData`](docs/api/parse-form-data.md) · [`validateValues`](docs/api/validate-values.md) · [`validateFieldValue`](docs/api/validate-field-value.md) |
|
|
146
146
|
|
|
147
|
+
Validation supports explicit cross-field dependencies with `dependsOn`, per-member collection rules with `each`, and strict decimal `numeric` / inclusive `min` / `max` rules. Async `onSubmit` handlers are reflected by `getState().isSubmitting`, while `addCleanup()` lets integrations register controller-owned teardown work.
|
|
148
|
+
|
|
147
149
|
See the full per-method reference in [`docs/api/`](docs/api/index.md).
|
|
148
150
|
|
|
149
151
|
---
|
package/dist/browser/global.d.ts
CHANGED
|
@@ -30,16 +30,29 @@ interface FieldValidationContext {
|
|
|
30
30
|
field: string;
|
|
31
31
|
value: FormFieldValue;
|
|
32
32
|
values: FormValues;
|
|
33
|
+
/** Concrete control while validating one member through `each`. */
|
|
34
|
+
element?: FormFieldElement;
|
|
35
|
+
/** Zero-based member position while validating through `each`. */
|
|
36
|
+
index?: number;
|
|
33
37
|
}
|
|
34
38
|
type FieldValidator = (context: FieldValidationContext) => string | undefined | null | false | true;
|
|
35
|
-
interface
|
|
39
|
+
interface ValueValidationRules {
|
|
36
40
|
required?: RuleConfig<boolean>;
|
|
37
41
|
minLength?: RuleConfig<number>;
|
|
38
42
|
maxLength?: RuleConfig<number>;
|
|
39
43
|
pattern?: RuleConfig<RegExp>;
|
|
44
|
+
numeric?: RuleConfig<boolean>;
|
|
45
|
+
min?: RuleConfig<number>;
|
|
46
|
+
max?: RuleConfig<number>;
|
|
47
|
+
validate?: FieldValidator | FieldValidator[];
|
|
48
|
+
}
|
|
49
|
+
interface FieldValidationRules extends ValueValidationRules {
|
|
40
50
|
/** Field name whose non-empty value must equal this field's value. */
|
|
41
51
|
sameAs?: RuleConfig<string>;
|
|
42
|
-
|
|
52
|
+
/** Fields whose changes should revalidate this field. */
|
|
53
|
+
dependsOn?: string | string[];
|
|
54
|
+
/** Rules applied independently to each collection member. */
|
|
55
|
+
each?: ValueValidationRules;
|
|
43
56
|
}
|
|
44
57
|
type ValidationSchema = Record<string, FieldValidationRules>;
|
|
45
58
|
interface VisualAttributes {
|
|
@@ -79,6 +92,7 @@ interface FormStateSnapshot {
|
|
|
79
92
|
isValid: boolean;
|
|
80
93
|
isValidated: boolean;
|
|
81
94
|
autoSubmit: boolean;
|
|
95
|
+
isSubmitting: boolean;
|
|
82
96
|
submitCount: number;
|
|
83
97
|
}
|
|
84
98
|
type FormStateListener = (state: FormStateSnapshot) => void;
|
|
@@ -93,12 +107,14 @@ interface AppendContentOptions {
|
|
|
93
107
|
atStart?: boolean;
|
|
94
108
|
}
|
|
95
109
|
type FormFieldWatcher = (value: FormFieldValue, field: FormFieldElement | FormFieldElement[] | null, form: HTMLFormElement, state: FormStateSnapshot) => void;
|
|
96
|
-
type FormSubmitHandler = (form: HTMLFormElement, data: Record<string, SerializedFormValue>, formData: FormData, state: FormStateSnapshot) => void
|
|
110
|
+
type FormSubmitHandler = (form: HTMLFormElement, data: Record<string, SerializedFormValue>, formData: FormData, state: FormStateSnapshot) => void | Promise<void>;
|
|
111
|
+
type FormCleanup = () => void;
|
|
97
112
|
interface FormController {
|
|
98
113
|
readonly element: HTMLFormElement | null;
|
|
99
114
|
readonly f: HTMLFormElement | null;
|
|
100
115
|
readonly options: FormControllerOptions;
|
|
101
116
|
onSubmit: (callback: FormSubmitHandler, preventDefault?: boolean) => FormController;
|
|
117
|
+
addCleanup: (cleanup: FormCleanup) => () => void;
|
|
102
118
|
watch: (field: string, callback: FormFieldWatcher) => FormController;
|
|
103
119
|
observe: (field: string, callback: FormFieldWatcher) => () => void;
|
|
104
120
|
unwatch: (field?: string, callback?: FormFieldWatcher) => FormController;
|
|
@@ -4788,6 +4788,22 @@ var Forms = (() => {
|
|
|
4788
4788
|
return node;
|
|
4789
4789
|
};
|
|
4790
4790
|
|
|
4791
|
+
// src/api/add-cleanup.ts
|
|
4792
|
+
var createAddCleanup = (state2, _helpers) => (cleanup) => {
|
|
4793
|
+
if (state2.isDestroyed) {
|
|
4794
|
+
cleanup();
|
|
4795
|
+
return () => void 0;
|
|
4796
|
+
}
|
|
4797
|
+
const registration = () => cleanup();
|
|
4798
|
+
state2.cleanups.add(registration);
|
|
4799
|
+
let active = true;
|
|
4800
|
+
return () => {
|
|
4801
|
+
if (!active) return;
|
|
4802
|
+
active = false;
|
|
4803
|
+
state2.cleanups.delete(registration);
|
|
4804
|
+
};
|
|
4805
|
+
};
|
|
4806
|
+
|
|
4791
4807
|
// src/api/auto-submit.ts
|
|
4792
4808
|
var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
4793
4809
|
state2.autoSubmitEnabled = next !== false;
|
|
@@ -5009,6 +5025,10 @@ var Forms = (() => {
|
|
|
5009
5025
|
...formatOptions ?? {},
|
|
5010
5026
|
interpretInputAs: "auto"
|
|
5011
5027
|
});
|
|
5028
|
+
var buildDisplayInterpretedOptions = (formatOptions) => ({
|
|
5029
|
+
...formatOptions ?? {},
|
|
5030
|
+
interpretInputAs: "display"
|
|
5031
|
+
});
|
|
5012
5032
|
var shouldForceAutoInterpretation = (formatOptions) => formatOptions?.interpretInputAs === void 0;
|
|
5013
5033
|
var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
|
|
5014
5034
|
const handler = (event) => {
|
|
@@ -5018,7 +5038,7 @@ var Forms = (() => {
|
|
|
5018
5038
|
if (!isVisible && !isMirror) return;
|
|
5019
5039
|
const rawInput = isMirror ? mirror.value : visible.value;
|
|
5020
5040
|
const inputType = readInputType(event);
|
|
5021
|
-
const sourceOptions =
|
|
5041
|
+
const sourceOptions = shouldForceAutoInterpretation(formatOptions) ? isMirror ? buildAutoInterpretedOptions(formatOptions) : buildDisplayInterpretedOptions(formatOptions) : formatOptions;
|
|
5022
5042
|
const { formatted, raw } = formatFn(rawInput, formatType, sourceOptions);
|
|
5023
5043
|
if (!formatted && !raw) {
|
|
5024
5044
|
visible.value = "";
|
|
@@ -5100,7 +5120,8 @@ var Forms = (() => {
|
|
|
5100
5120
|
if (entry.handler !== UNBOUND) {
|
|
5101
5121
|
const current = entry.visible.value;
|
|
5102
5122
|
if (current !== "") {
|
|
5103
|
-
const
|
|
5123
|
+
const rebindOptions = shouldForceAutoInterpretation(formatOptions) ? buildDisplayInterpretedOptions(formatOptions) : formatOptions;
|
|
5124
|
+
const { formatted, raw } = formatter.format(current, formatType, rebindOptions);
|
|
5104
5125
|
applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
|
|
5105
5126
|
}
|
|
5106
5127
|
continue;
|
|
@@ -5215,12 +5236,24 @@ var Forms = (() => {
|
|
|
5215
5236
|
}
|
|
5216
5237
|
state2.mutationObserver?.disconnect();
|
|
5217
5238
|
state2.mutationObserver = null;
|
|
5239
|
+
const cleanups = Array.from(state2.cleanups).reverse();
|
|
5240
|
+
state2.cleanups.clear();
|
|
5241
|
+
for (const cleanup of cleanups) {
|
|
5242
|
+
try {
|
|
5243
|
+
cleanup();
|
|
5244
|
+
} catch (error) {
|
|
5245
|
+
console.error("[forms] cleanup failed", error);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5218
5248
|
state2.watchedFields.clear();
|
|
5219
5249
|
state2.subscribers.clear();
|
|
5220
5250
|
state2.submitHandlers.clear();
|
|
5221
5251
|
state2.fieldCache.clear();
|
|
5222
5252
|
state2.manualErrors = {};
|
|
5223
5253
|
state2.validationErrors = {};
|
|
5254
|
+
state2.validationGroupErrors = {};
|
|
5255
|
+
state2.validationElementErrors.clear();
|
|
5256
|
+
state2.pendingSubmitCount = 0;
|
|
5224
5257
|
cleanupFormatRegistry(state2);
|
|
5225
5258
|
};
|
|
5226
5259
|
|
|
@@ -5287,6 +5320,7 @@ var Forms = (() => {
|
|
|
5287
5320
|
isValid: true,
|
|
5288
5321
|
isValidated: false,
|
|
5289
5322
|
autoSubmit: false,
|
|
5323
|
+
isSubmitting: false,
|
|
5290
5324
|
submitCount: 0
|
|
5291
5325
|
});
|
|
5292
5326
|
var cloneErrors = (errors) => Object.fromEntries(
|
|
@@ -5318,6 +5352,7 @@ var Forms = (() => {
|
|
|
5318
5352
|
isValid: Object.keys(errors).length === 0,
|
|
5319
5353
|
isValidated: state2.isValidated,
|
|
5320
5354
|
autoSubmit: state2.autoSubmitEnabled,
|
|
5355
|
+
isSubmitting: state2.pendingSubmitCount > 0,
|
|
5321
5356
|
submitCount: state2.submitCount
|
|
5322
5357
|
};
|
|
5323
5358
|
};
|
|
@@ -5515,12 +5550,14 @@ var Forms = (() => {
|
|
|
5515
5550
|
resetFormattedFields(state2);
|
|
5516
5551
|
state2.manualErrors = {};
|
|
5517
5552
|
state2.validationErrors = {};
|
|
5553
|
+
state2.validationGroupErrors = {};
|
|
5554
|
+
state2.validationElementErrors.clear();
|
|
5518
5555
|
clearAttributes(state2.element, [
|
|
5519
5556
|
state2.attributes.error,
|
|
5520
5557
|
state2.attributes.filled,
|
|
5521
5558
|
"aria-invalid"
|
|
5522
5559
|
]);
|
|
5523
|
-
|
|
5560
|
+
helpers.syncVisualState();
|
|
5524
5561
|
helpers.notifySubscribers();
|
|
5525
5562
|
return state2.api;
|
|
5526
5563
|
};
|
|
@@ -5631,14 +5668,29 @@ var Forms = (() => {
|
|
|
5631
5668
|
}
|
|
5632
5669
|
return left === right;
|
|
5633
5670
|
};
|
|
5634
|
-
var
|
|
5671
|
+
var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
5672
|
+
var parseStrictNumber = (value) => {
|
|
5673
|
+
if (typeof value !== "string") return void 0;
|
|
5674
|
+
const candidate = value.trim();
|
|
5675
|
+
if (!DECIMAL_PATTERN.test(candidate)) return void 0;
|
|
5676
|
+
const number = Number(candidate);
|
|
5677
|
+
return Number.isFinite(number) ? number : void 0;
|
|
5678
|
+
};
|
|
5679
|
+
var validateValue = (field, value, rules, values, contextOverrides = {}) => {
|
|
5635
5680
|
const errors = [];
|
|
5636
|
-
const context = {
|
|
5681
|
+
const context = {
|
|
5682
|
+
field,
|
|
5683
|
+
value,
|
|
5684
|
+
values,
|
|
5685
|
+
...contextOverrides
|
|
5686
|
+
};
|
|
5637
5687
|
const required = resolveRule(rules.required);
|
|
5638
5688
|
const minLength = resolveRule(rules.minLength);
|
|
5639
5689
|
const maxLength = resolveRule(rules.maxLength);
|
|
5640
5690
|
const pattern = resolveRule(rules.pattern);
|
|
5641
|
-
const
|
|
5691
|
+
const numeric = resolveRule(rules.numeric);
|
|
5692
|
+
const min = resolveRule(rules.min);
|
|
5693
|
+
const max = resolveRule(rules.max);
|
|
5642
5694
|
if (required.value && !hasValue(value)) {
|
|
5643
5695
|
errors.push(required.message ?? "This field is required.");
|
|
5644
5696
|
}
|
|
@@ -5656,10 +5708,17 @@ var Forms = (() => {
|
|
|
5656
5708
|
errors.push(pattern.message ?? "Value does not match the required pattern.");
|
|
5657
5709
|
}
|
|
5658
5710
|
}
|
|
5659
|
-
if (
|
|
5660
|
-
const
|
|
5661
|
-
if (
|
|
5662
|
-
errors.push(
|
|
5711
|
+
if (hasValue(value) && (numeric.value || min.value !== void 0 || max.value !== void 0)) {
|
|
5712
|
+
const number = parseStrictNumber(value);
|
|
5713
|
+
if (number === void 0) {
|
|
5714
|
+
errors.push(numeric.message ?? "Value must be a number.");
|
|
5715
|
+
} else {
|
|
5716
|
+
if (min.value !== void 0 && number < min.value) {
|
|
5717
|
+
errors.push(min.message ?? `Minimum value is ${min.value}.`);
|
|
5718
|
+
}
|
|
5719
|
+
if (max.value !== void 0 && number > max.value) {
|
|
5720
|
+
errors.push(max.message ?? `Maximum value is ${max.value}.`);
|
|
5721
|
+
}
|
|
5663
5722
|
}
|
|
5664
5723
|
}
|
|
5665
5724
|
const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
|
|
@@ -5670,6 +5729,37 @@ var Forms = (() => {
|
|
|
5670
5729
|
}
|
|
5671
5730
|
return errors;
|
|
5672
5731
|
};
|
|
5732
|
+
var validateFieldValueDetailed = (field, value, rules, values, items) => {
|
|
5733
|
+
const groupErrors = validateValue(field, value, rules, values);
|
|
5734
|
+
const sameAs = resolveRule(rules.sameAs);
|
|
5735
|
+
if (sameAs.value !== void 0) {
|
|
5736
|
+
const otherValue = values[sameAs.value];
|
|
5737
|
+
if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
|
|
5738
|
+
groupErrors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
const itemErrors = [];
|
|
5742
|
+
if (rules.each) {
|
|
5743
|
+
const sourceItems = items ?? (Array.isArray(value) ? value.map((entry) => ({
|
|
5744
|
+
value: typeof entry === "string" ? entry : [entry]
|
|
5745
|
+
})) : [{ value }]);
|
|
5746
|
+
sourceItems.forEach((item, index) => {
|
|
5747
|
+
const overrides = {
|
|
5748
|
+
index
|
|
5749
|
+
};
|
|
5750
|
+
if (item.element) overrides.element = item.element;
|
|
5751
|
+
const messages = validateValue(field, item.value, rules.each, values, overrides);
|
|
5752
|
+
if (messages.length === 0) return;
|
|
5753
|
+
const error = {
|
|
5754
|
+
index,
|
|
5755
|
+
messages
|
|
5756
|
+
};
|
|
5757
|
+
if (item.element) error.element = item.element;
|
|
5758
|
+
itemErrors.push(error);
|
|
5759
|
+
});
|
|
5760
|
+
}
|
|
5761
|
+
return { groupErrors, itemErrors };
|
|
5762
|
+
};
|
|
5673
5763
|
|
|
5674
5764
|
// src/core/controller.ts
|
|
5675
5765
|
var DEFAULT_ATTRIBUTES = {
|
|
@@ -5700,25 +5790,35 @@ var Forms = (() => {
|
|
|
5700
5790
|
fieldCache: /* @__PURE__ */ new Map(),
|
|
5701
5791
|
manualErrors: {},
|
|
5702
5792
|
validationErrors: {},
|
|
5793
|
+
validationGroupErrors: {},
|
|
5794
|
+
validationElementErrors: /* @__PURE__ */ new Map(),
|
|
5703
5795
|
isValidated: Boolean(normalizedOptions.autoValidate),
|
|
5704
5796
|
autoSubmitEnabled: false,
|
|
5705
5797
|
autoSubmitDebounce: 0,
|
|
5706
5798
|
submitCount: 0,
|
|
5799
|
+
pendingSubmitCount: 0,
|
|
5707
5800
|
autoSubmitTimer: null,
|
|
5708
5801
|
isDestroyed: false,
|
|
5709
5802
|
listeners: [],
|
|
5710
5803
|
mutationObserver: null,
|
|
5804
|
+
cleanups: /* @__PURE__ */ new Set(),
|
|
5711
5805
|
formattedFields: /* @__PURE__ */ new Map(),
|
|
5712
5806
|
api: null
|
|
5713
5807
|
};
|
|
5714
5808
|
const validationDependents = /* @__PURE__ */ new Map();
|
|
5715
5809
|
for (const [field, rules] of Object.entries(state2.validators)) {
|
|
5810
|
+
const sources = /* @__PURE__ */ new Set();
|
|
5716
5811
|
const sameAs = rules.sameAs;
|
|
5717
|
-
if (sameAs
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5812
|
+
if (sameAs !== void 0) {
|
|
5813
|
+
sources.add(typeof sameAs === "string" ? sameAs : sameAs.value);
|
|
5814
|
+
}
|
|
5815
|
+
if (typeof rules.dependsOn === "string") sources.add(rules.dependsOn);
|
|
5816
|
+
else rules.dependsOn?.forEach((source) => sources.add(source));
|
|
5817
|
+
for (const source of sources) {
|
|
5818
|
+
const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
|
|
5819
|
+
dependents.add(field);
|
|
5820
|
+
validationDependents.set(source, dependents);
|
|
5821
|
+
}
|
|
5722
5822
|
}
|
|
5723
5823
|
const notifySubscribers = () => {
|
|
5724
5824
|
if (!state2.api) return;
|
|
@@ -5767,18 +5867,21 @@ var Forms = (() => {
|
|
|
5767
5867
|
const syncVisualState = (names) => {
|
|
5768
5868
|
if (!state2.element) return;
|
|
5769
5869
|
const sourceNames = names ?? getTrackedFieldNames();
|
|
5770
|
-
const errors = getMergedErrors();
|
|
5771
5870
|
for (const name of sourceNames) {
|
|
5772
5871
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
5773
5872
|
const canonical = resolveCanonicalForName(state2, name) ?? name;
|
|
5774
5873
|
const fields = getFieldsByName(displayName);
|
|
5775
|
-
const
|
|
5776
|
-
|
|
5874
|
+
const hasGroupError = Boolean(
|
|
5875
|
+
state2.validationGroupErrors[canonical]?.length || state2.manualErrors[canonical]?.length
|
|
5876
|
+
);
|
|
5877
|
+
const elementErrors = state2.validationElementErrors.get(canonical);
|
|
5878
|
+
fields.forEach((field) => {
|
|
5879
|
+
const hasError = hasGroupError || Boolean(elementErrors?.get(field)?.length);
|
|
5777
5880
|
applyBooleanAttribute(field, attributes.filled, isFieldFilled(field));
|
|
5778
5881
|
applyBooleanAttribute(field, attributes.error, hasError);
|
|
5779
5882
|
if (hasError) field.setAttribute("aria-invalid", "true");
|
|
5780
5883
|
else field.removeAttribute("aria-invalid");
|
|
5781
|
-
}
|
|
5884
|
+
});
|
|
5782
5885
|
}
|
|
5783
5886
|
};
|
|
5784
5887
|
void readFieldValue;
|
|
@@ -5787,17 +5890,38 @@ var Forms = (() => {
|
|
|
5787
5890
|
const targetNames = names ?? Object.keys(state2.validators);
|
|
5788
5891
|
const values = getValues();
|
|
5789
5892
|
const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
|
|
5893
|
+
const nextGroupErrors = names ? cloneErrors(state2.validationGroupErrors) : {};
|
|
5894
|
+
const nextElementErrors = names ? new Map(state2.validationElementErrors) : /* @__PURE__ */ new Map();
|
|
5790
5895
|
for (const name of targetNames) {
|
|
5791
5896
|
const rules = state2.validators[name];
|
|
5792
5897
|
if (!rules) {
|
|
5793
5898
|
delete nextValidationErrors[name];
|
|
5899
|
+
delete nextGroupErrors[name];
|
|
5900
|
+
nextElementErrors.delete(name);
|
|
5794
5901
|
continue;
|
|
5795
5902
|
}
|
|
5796
|
-
const
|
|
5903
|
+
const fields = getFieldsByName(name);
|
|
5904
|
+
const displayFields = getFieldsByName(resolveDisplayNameForName(state2, name));
|
|
5905
|
+
const items = fields.map((field, index) => ({
|
|
5906
|
+
value: readFieldValue([field]),
|
|
5907
|
+
element: displayFields[index] ?? field
|
|
5908
|
+
}));
|
|
5909
|
+
const result = validateFieldValueDetailed(name, values[name], rules, values, items);
|
|
5910
|
+
const messages = [...result.groupErrors, ...result.itemErrors.flatMap((item) => item.messages)];
|
|
5797
5911
|
if (messages.length > 0) nextValidationErrors[name] = messages;
|
|
5798
5912
|
else delete nextValidationErrors[name];
|
|
5913
|
+
if (result.groupErrors.length > 0) nextGroupErrors[name] = result.groupErrors;
|
|
5914
|
+
else delete nextGroupErrors[name];
|
|
5915
|
+
const perElement = /* @__PURE__ */ new Map();
|
|
5916
|
+
result.itemErrors.forEach((item) => {
|
|
5917
|
+
if (item.element) perElement.set(item.element, item.messages);
|
|
5918
|
+
});
|
|
5919
|
+
if (perElement.size > 0) nextElementErrors.set(name, perElement);
|
|
5920
|
+
else nextElementErrors.delete(name);
|
|
5799
5921
|
}
|
|
5800
5922
|
state2.validationErrors = nextValidationErrors;
|
|
5923
|
+
state2.validationGroupErrors = nextGroupErrors;
|
|
5924
|
+
state2.validationElementErrors = nextElementErrors;
|
|
5801
5925
|
syncVisualState(names);
|
|
5802
5926
|
return {
|
|
5803
5927
|
isValid: Object.keys(getMergedErrors()).length === 0,
|
|
@@ -5891,7 +6015,13 @@ var Forms = (() => {
|
|
|
5891
6015
|
if (!validation.isValid) {
|
|
5892
6016
|
for (const name of Object.keys(validation.errors)) {
|
|
5893
6017
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
5894
|
-
const
|
|
6018
|
+
const fields = getFieldsByName(displayName);
|
|
6019
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
6020
|
+
const field = fields.find((candidate2) => {
|
|
6021
|
+
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
6022
|
+
if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
|
|
6023
|
+
return elementErrors?.has(candidate2);
|
|
6024
|
+
}) ?? fields.find((candidate2) => {
|
|
5895
6025
|
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
5896
6026
|
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
5897
6027
|
});
|
|
@@ -5906,18 +6036,35 @@ var Forms = (() => {
|
|
|
5906
6036
|
const submitter = candidate.submitter instanceof HTMLElement ? candidate.submitter : null;
|
|
5907
6037
|
const { data, formData } = parseFormData(state2.element, submitter);
|
|
5908
6038
|
const snapshot = state2.api.getState();
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
6039
|
+
const pending = [];
|
|
6040
|
+
let synchronousError;
|
|
6041
|
+
let hasSynchronousError = false;
|
|
6042
|
+
try {
|
|
6043
|
+
for (const handler of handlers) {
|
|
6044
|
+
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
6045
|
+
if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
|
|
6046
|
+
}
|
|
6047
|
+
} catch (error) {
|
|
6048
|
+
synchronousError = error;
|
|
6049
|
+
hasSynchronousError = true;
|
|
6050
|
+
}
|
|
6051
|
+
if (pending.length > 0) {
|
|
6052
|
+
state2.pendingSubmitCount += 1;
|
|
6053
|
+
notifySubscribers();
|
|
6054
|
+
void Promise.allSettled(pending).then(() => {
|
|
6055
|
+
state2.pendingSubmitCount = Math.max(0, state2.pendingSubmitCount - 1);
|
|
6056
|
+
if (!state2.isDestroyed) notifySubscribers();
|
|
6057
|
+
});
|
|
6058
|
+
}
|
|
6059
|
+
if (hasSynchronousError) throw synchronousError;
|
|
5912
6060
|
};
|
|
5913
6061
|
const startMutationObserver = () => {
|
|
5914
6062
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
5915
6063
|
state2.mutationObserver = new MutationObserver(() => {
|
|
5916
6064
|
clearFieldCache();
|
|
5917
6065
|
if (state2.isValidated) {
|
|
5918
|
-
syncVisualState();
|
|
5919
6066
|
validateNames();
|
|
5920
|
-
}
|
|
6067
|
+
} else syncVisualState();
|
|
5921
6068
|
notifySubscribers();
|
|
5922
6069
|
});
|
|
5923
6070
|
state2.mutationObserver.observe(state2.element, {
|
|
@@ -5938,6 +6085,7 @@ var Forms = (() => {
|
|
|
5938
6085
|
return state2.options;
|
|
5939
6086
|
},
|
|
5940
6087
|
onSubmit: createOnSubmit(state2, helpers),
|
|
6088
|
+
addCleanup: createAddCleanup(state2, helpers),
|
|
5941
6089
|
watch: createWatch(state2, helpers),
|
|
5942
6090
|
observe: createObserve(state2, helpers),
|
|
5943
6091
|
unwatch: createUnwatch(state2, helpers),
|
|
@@ -5975,10 +6123,8 @@ var Forms = (() => {
|
|
|
5975
6123
|
api.format(config);
|
|
5976
6124
|
}
|
|
5977
6125
|
}
|
|
5978
|
-
if (state2.isValidated)
|
|
5979
|
-
|
|
5980
|
-
validateNames();
|
|
5981
|
-
}
|
|
6126
|
+
if (state2.isValidated) validateNames();
|
|
6127
|
+
else syncVisualState();
|
|
5982
6128
|
return api;
|
|
5983
6129
|
};
|
|
5984
6130
|
|