@samline/forms 2.6.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/README.md +6 -4
- package/dist/browser/global.d.ts +19 -3
- package/dist/browser/global.global.js +169 -28
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +169 -28
- 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-WGPAVJTK.js} +174 -29
- package/dist/chunk-WGPAVJTK.js.map +1 -0
- package/dist/index.cjs +173 -28
- 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/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 +64 -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.0/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.0` 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.0/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;
|
|
@@ -5215,12 +5231,24 @@ var Forms = (() => {
|
|
|
5215
5231
|
}
|
|
5216
5232
|
state2.mutationObserver?.disconnect();
|
|
5217
5233
|
state2.mutationObserver = null;
|
|
5234
|
+
const cleanups = Array.from(state2.cleanups).reverse();
|
|
5235
|
+
state2.cleanups.clear();
|
|
5236
|
+
for (const cleanup of cleanups) {
|
|
5237
|
+
try {
|
|
5238
|
+
cleanup();
|
|
5239
|
+
} catch (error) {
|
|
5240
|
+
console.error("[forms] cleanup failed", error);
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5218
5243
|
state2.watchedFields.clear();
|
|
5219
5244
|
state2.subscribers.clear();
|
|
5220
5245
|
state2.submitHandlers.clear();
|
|
5221
5246
|
state2.fieldCache.clear();
|
|
5222
5247
|
state2.manualErrors = {};
|
|
5223
5248
|
state2.validationErrors = {};
|
|
5249
|
+
state2.validationGroupErrors = {};
|
|
5250
|
+
state2.validationElementErrors.clear();
|
|
5251
|
+
state2.pendingSubmitCount = 0;
|
|
5224
5252
|
cleanupFormatRegistry(state2);
|
|
5225
5253
|
};
|
|
5226
5254
|
|
|
@@ -5287,6 +5315,7 @@ var Forms = (() => {
|
|
|
5287
5315
|
isValid: true,
|
|
5288
5316
|
isValidated: false,
|
|
5289
5317
|
autoSubmit: false,
|
|
5318
|
+
isSubmitting: false,
|
|
5290
5319
|
submitCount: 0
|
|
5291
5320
|
});
|
|
5292
5321
|
var cloneErrors = (errors) => Object.fromEntries(
|
|
@@ -5318,6 +5347,7 @@ var Forms = (() => {
|
|
|
5318
5347
|
isValid: Object.keys(errors).length === 0,
|
|
5319
5348
|
isValidated: state2.isValidated,
|
|
5320
5349
|
autoSubmit: state2.autoSubmitEnabled,
|
|
5350
|
+
isSubmitting: state2.pendingSubmitCount > 0,
|
|
5321
5351
|
submitCount: state2.submitCount
|
|
5322
5352
|
};
|
|
5323
5353
|
};
|
|
@@ -5515,12 +5545,14 @@ var Forms = (() => {
|
|
|
5515
5545
|
resetFormattedFields(state2);
|
|
5516
5546
|
state2.manualErrors = {};
|
|
5517
5547
|
state2.validationErrors = {};
|
|
5548
|
+
state2.validationGroupErrors = {};
|
|
5549
|
+
state2.validationElementErrors.clear();
|
|
5518
5550
|
clearAttributes(state2.element, [
|
|
5519
5551
|
state2.attributes.error,
|
|
5520
5552
|
state2.attributes.filled,
|
|
5521
5553
|
"aria-invalid"
|
|
5522
5554
|
]);
|
|
5523
|
-
|
|
5555
|
+
helpers.syncVisualState();
|
|
5524
5556
|
helpers.notifySubscribers();
|
|
5525
5557
|
return state2.api;
|
|
5526
5558
|
};
|
|
@@ -5631,14 +5663,29 @@ var Forms = (() => {
|
|
|
5631
5663
|
}
|
|
5632
5664
|
return left === right;
|
|
5633
5665
|
};
|
|
5634
|
-
var
|
|
5666
|
+
var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
5667
|
+
var parseStrictNumber = (value) => {
|
|
5668
|
+
if (typeof value !== "string") return void 0;
|
|
5669
|
+
const candidate = value.trim();
|
|
5670
|
+
if (!DECIMAL_PATTERN.test(candidate)) return void 0;
|
|
5671
|
+
const number = Number(candidate);
|
|
5672
|
+
return Number.isFinite(number) ? number : void 0;
|
|
5673
|
+
};
|
|
5674
|
+
var validateValue = (field, value, rules, values, contextOverrides = {}) => {
|
|
5635
5675
|
const errors = [];
|
|
5636
|
-
const context = {
|
|
5676
|
+
const context = {
|
|
5677
|
+
field,
|
|
5678
|
+
value,
|
|
5679
|
+
values,
|
|
5680
|
+
...contextOverrides
|
|
5681
|
+
};
|
|
5637
5682
|
const required = resolveRule(rules.required);
|
|
5638
5683
|
const minLength = resolveRule(rules.minLength);
|
|
5639
5684
|
const maxLength = resolveRule(rules.maxLength);
|
|
5640
5685
|
const pattern = resolveRule(rules.pattern);
|
|
5641
|
-
const
|
|
5686
|
+
const numeric = resolveRule(rules.numeric);
|
|
5687
|
+
const min = resolveRule(rules.min);
|
|
5688
|
+
const max = resolveRule(rules.max);
|
|
5642
5689
|
if (required.value && !hasValue(value)) {
|
|
5643
5690
|
errors.push(required.message ?? "This field is required.");
|
|
5644
5691
|
}
|
|
@@ -5656,10 +5703,17 @@ var Forms = (() => {
|
|
|
5656
5703
|
errors.push(pattern.message ?? "Value does not match the required pattern.");
|
|
5657
5704
|
}
|
|
5658
5705
|
}
|
|
5659
|
-
if (
|
|
5660
|
-
const
|
|
5661
|
-
if (
|
|
5662
|
-
errors.push(
|
|
5706
|
+
if (hasValue(value) && (numeric.value || min.value !== void 0 || max.value !== void 0)) {
|
|
5707
|
+
const number = parseStrictNumber(value);
|
|
5708
|
+
if (number === void 0) {
|
|
5709
|
+
errors.push(numeric.message ?? "Value must be a number.");
|
|
5710
|
+
} else {
|
|
5711
|
+
if (min.value !== void 0 && number < min.value) {
|
|
5712
|
+
errors.push(min.message ?? `Minimum value is ${min.value}.`);
|
|
5713
|
+
}
|
|
5714
|
+
if (max.value !== void 0 && number > max.value) {
|
|
5715
|
+
errors.push(max.message ?? `Maximum value is ${max.value}.`);
|
|
5716
|
+
}
|
|
5663
5717
|
}
|
|
5664
5718
|
}
|
|
5665
5719
|
const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
|
|
@@ -5670,6 +5724,37 @@ var Forms = (() => {
|
|
|
5670
5724
|
}
|
|
5671
5725
|
return errors;
|
|
5672
5726
|
};
|
|
5727
|
+
var validateFieldValueDetailed = (field, value, rules, values, items) => {
|
|
5728
|
+
const groupErrors = validateValue(field, value, rules, values);
|
|
5729
|
+
const sameAs = resolveRule(rules.sameAs);
|
|
5730
|
+
if (sameAs.value !== void 0) {
|
|
5731
|
+
const otherValue = values[sameAs.value];
|
|
5732
|
+
if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
|
|
5733
|
+
groupErrors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
const itemErrors = [];
|
|
5737
|
+
if (rules.each) {
|
|
5738
|
+
const sourceItems = items ?? (Array.isArray(value) ? value.map((entry) => ({
|
|
5739
|
+
value: typeof entry === "string" ? entry : [entry]
|
|
5740
|
+
})) : [{ value }]);
|
|
5741
|
+
sourceItems.forEach((item, index) => {
|
|
5742
|
+
const overrides = {
|
|
5743
|
+
index
|
|
5744
|
+
};
|
|
5745
|
+
if (item.element) overrides.element = item.element;
|
|
5746
|
+
const messages = validateValue(field, item.value, rules.each, values, overrides);
|
|
5747
|
+
if (messages.length === 0) return;
|
|
5748
|
+
const error = {
|
|
5749
|
+
index,
|
|
5750
|
+
messages
|
|
5751
|
+
};
|
|
5752
|
+
if (item.element) error.element = item.element;
|
|
5753
|
+
itemErrors.push(error);
|
|
5754
|
+
});
|
|
5755
|
+
}
|
|
5756
|
+
return { groupErrors, itemErrors };
|
|
5757
|
+
};
|
|
5673
5758
|
|
|
5674
5759
|
// src/core/controller.ts
|
|
5675
5760
|
var DEFAULT_ATTRIBUTES = {
|
|
@@ -5700,25 +5785,35 @@ var Forms = (() => {
|
|
|
5700
5785
|
fieldCache: /* @__PURE__ */ new Map(),
|
|
5701
5786
|
manualErrors: {},
|
|
5702
5787
|
validationErrors: {},
|
|
5788
|
+
validationGroupErrors: {},
|
|
5789
|
+
validationElementErrors: /* @__PURE__ */ new Map(),
|
|
5703
5790
|
isValidated: Boolean(normalizedOptions.autoValidate),
|
|
5704
5791
|
autoSubmitEnabled: false,
|
|
5705
5792
|
autoSubmitDebounce: 0,
|
|
5706
5793
|
submitCount: 0,
|
|
5794
|
+
pendingSubmitCount: 0,
|
|
5707
5795
|
autoSubmitTimer: null,
|
|
5708
5796
|
isDestroyed: false,
|
|
5709
5797
|
listeners: [],
|
|
5710
5798
|
mutationObserver: null,
|
|
5799
|
+
cleanups: /* @__PURE__ */ new Set(),
|
|
5711
5800
|
formattedFields: /* @__PURE__ */ new Map(),
|
|
5712
5801
|
api: null
|
|
5713
5802
|
};
|
|
5714
5803
|
const validationDependents = /* @__PURE__ */ new Map();
|
|
5715
5804
|
for (const [field, rules] of Object.entries(state2.validators)) {
|
|
5805
|
+
const sources = /* @__PURE__ */ new Set();
|
|
5716
5806
|
const sameAs = rules.sameAs;
|
|
5717
|
-
if (sameAs
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5807
|
+
if (sameAs !== void 0) {
|
|
5808
|
+
sources.add(typeof sameAs === "string" ? sameAs : sameAs.value);
|
|
5809
|
+
}
|
|
5810
|
+
if (typeof rules.dependsOn === "string") sources.add(rules.dependsOn);
|
|
5811
|
+
else rules.dependsOn?.forEach((source) => sources.add(source));
|
|
5812
|
+
for (const source of sources) {
|
|
5813
|
+
const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
|
|
5814
|
+
dependents.add(field);
|
|
5815
|
+
validationDependents.set(source, dependents);
|
|
5816
|
+
}
|
|
5722
5817
|
}
|
|
5723
5818
|
const notifySubscribers = () => {
|
|
5724
5819
|
if (!state2.api) return;
|
|
@@ -5767,18 +5862,21 @@ var Forms = (() => {
|
|
|
5767
5862
|
const syncVisualState = (names) => {
|
|
5768
5863
|
if (!state2.element) return;
|
|
5769
5864
|
const sourceNames = names ?? getTrackedFieldNames();
|
|
5770
|
-
const errors = getMergedErrors();
|
|
5771
5865
|
for (const name of sourceNames) {
|
|
5772
5866
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
5773
5867
|
const canonical = resolveCanonicalForName(state2, name) ?? name;
|
|
5774
5868
|
const fields = getFieldsByName(displayName);
|
|
5775
|
-
const
|
|
5776
|
-
|
|
5869
|
+
const hasGroupError = Boolean(
|
|
5870
|
+
state2.validationGroupErrors[canonical]?.length || state2.manualErrors[canonical]?.length
|
|
5871
|
+
);
|
|
5872
|
+
const elementErrors = state2.validationElementErrors.get(canonical);
|
|
5873
|
+
fields.forEach((field) => {
|
|
5874
|
+
const hasError = hasGroupError || Boolean(elementErrors?.get(field)?.length);
|
|
5777
5875
|
applyBooleanAttribute(field, attributes.filled, isFieldFilled(field));
|
|
5778
5876
|
applyBooleanAttribute(field, attributes.error, hasError);
|
|
5779
5877
|
if (hasError) field.setAttribute("aria-invalid", "true");
|
|
5780
5878
|
else field.removeAttribute("aria-invalid");
|
|
5781
|
-
}
|
|
5879
|
+
});
|
|
5782
5880
|
}
|
|
5783
5881
|
};
|
|
5784
5882
|
void readFieldValue;
|
|
@@ -5787,17 +5885,38 @@ var Forms = (() => {
|
|
|
5787
5885
|
const targetNames = names ?? Object.keys(state2.validators);
|
|
5788
5886
|
const values = getValues();
|
|
5789
5887
|
const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
|
|
5888
|
+
const nextGroupErrors = names ? cloneErrors(state2.validationGroupErrors) : {};
|
|
5889
|
+
const nextElementErrors = names ? new Map(state2.validationElementErrors) : /* @__PURE__ */ new Map();
|
|
5790
5890
|
for (const name of targetNames) {
|
|
5791
5891
|
const rules = state2.validators[name];
|
|
5792
5892
|
if (!rules) {
|
|
5793
5893
|
delete nextValidationErrors[name];
|
|
5894
|
+
delete nextGroupErrors[name];
|
|
5895
|
+
nextElementErrors.delete(name);
|
|
5794
5896
|
continue;
|
|
5795
5897
|
}
|
|
5796
|
-
const
|
|
5898
|
+
const fields = getFieldsByName(name);
|
|
5899
|
+
const displayFields = getFieldsByName(resolveDisplayNameForName(state2, name));
|
|
5900
|
+
const items = fields.map((field, index) => ({
|
|
5901
|
+
value: readFieldValue([field]),
|
|
5902
|
+
element: displayFields[index] ?? field
|
|
5903
|
+
}));
|
|
5904
|
+
const result = validateFieldValueDetailed(name, values[name], rules, values, items);
|
|
5905
|
+
const messages = [...result.groupErrors, ...result.itemErrors.flatMap((item) => item.messages)];
|
|
5797
5906
|
if (messages.length > 0) nextValidationErrors[name] = messages;
|
|
5798
5907
|
else delete nextValidationErrors[name];
|
|
5908
|
+
if (result.groupErrors.length > 0) nextGroupErrors[name] = result.groupErrors;
|
|
5909
|
+
else delete nextGroupErrors[name];
|
|
5910
|
+
const perElement = /* @__PURE__ */ new Map();
|
|
5911
|
+
result.itemErrors.forEach((item) => {
|
|
5912
|
+
if (item.element) perElement.set(item.element, item.messages);
|
|
5913
|
+
});
|
|
5914
|
+
if (perElement.size > 0) nextElementErrors.set(name, perElement);
|
|
5915
|
+
else nextElementErrors.delete(name);
|
|
5799
5916
|
}
|
|
5800
5917
|
state2.validationErrors = nextValidationErrors;
|
|
5918
|
+
state2.validationGroupErrors = nextGroupErrors;
|
|
5919
|
+
state2.validationElementErrors = nextElementErrors;
|
|
5801
5920
|
syncVisualState(names);
|
|
5802
5921
|
return {
|
|
5803
5922
|
isValid: Object.keys(getMergedErrors()).length === 0,
|
|
@@ -5891,7 +6010,13 @@ var Forms = (() => {
|
|
|
5891
6010
|
if (!validation.isValid) {
|
|
5892
6011
|
for (const name of Object.keys(validation.errors)) {
|
|
5893
6012
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
5894
|
-
const
|
|
6013
|
+
const fields = getFieldsByName(displayName);
|
|
6014
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
6015
|
+
const field = fields.find((candidate2) => {
|
|
6016
|
+
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
6017
|
+
if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
|
|
6018
|
+
return elementErrors?.has(candidate2);
|
|
6019
|
+
}) ?? fields.find((candidate2) => {
|
|
5895
6020
|
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
5896
6021
|
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
5897
6022
|
});
|
|
@@ -5906,18 +6031,35 @@ var Forms = (() => {
|
|
|
5906
6031
|
const submitter = candidate.submitter instanceof HTMLElement ? candidate.submitter : null;
|
|
5907
6032
|
const { data, formData } = parseFormData(state2.element, submitter);
|
|
5908
6033
|
const snapshot = state2.api.getState();
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
6034
|
+
const pending = [];
|
|
6035
|
+
let synchronousError;
|
|
6036
|
+
let hasSynchronousError = false;
|
|
6037
|
+
try {
|
|
6038
|
+
for (const handler of handlers) {
|
|
6039
|
+
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
6040
|
+
if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
|
|
6041
|
+
}
|
|
6042
|
+
} catch (error) {
|
|
6043
|
+
synchronousError = error;
|
|
6044
|
+
hasSynchronousError = true;
|
|
6045
|
+
}
|
|
6046
|
+
if (pending.length > 0) {
|
|
6047
|
+
state2.pendingSubmitCount += 1;
|
|
6048
|
+
notifySubscribers();
|
|
6049
|
+
void Promise.allSettled(pending).then(() => {
|
|
6050
|
+
state2.pendingSubmitCount = Math.max(0, state2.pendingSubmitCount - 1);
|
|
6051
|
+
if (!state2.isDestroyed) notifySubscribers();
|
|
6052
|
+
});
|
|
6053
|
+
}
|
|
6054
|
+
if (hasSynchronousError) throw synchronousError;
|
|
5912
6055
|
};
|
|
5913
6056
|
const startMutationObserver = () => {
|
|
5914
6057
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
5915
6058
|
state2.mutationObserver = new MutationObserver(() => {
|
|
5916
6059
|
clearFieldCache();
|
|
5917
6060
|
if (state2.isValidated) {
|
|
5918
|
-
syncVisualState();
|
|
5919
6061
|
validateNames();
|
|
5920
|
-
}
|
|
6062
|
+
} else syncVisualState();
|
|
5921
6063
|
notifySubscribers();
|
|
5922
6064
|
});
|
|
5923
6065
|
state2.mutationObserver.observe(state2.element, {
|
|
@@ -5938,6 +6080,7 @@ var Forms = (() => {
|
|
|
5938
6080
|
return state2.options;
|
|
5939
6081
|
},
|
|
5940
6082
|
onSubmit: createOnSubmit(state2, helpers),
|
|
6083
|
+
addCleanup: createAddCleanup(state2, helpers),
|
|
5941
6084
|
watch: createWatch(state2, helpers),
|
|
5942
6085
|
observe: createObserve(state2, helpers),
|
|
5943
6086
|
unwatch: createUnwatch(state2, helpers),
|
|
@@ -5975,10 +6118,8 @@ var Forms = (() => {
|
|
|
5975
6118
|
api.format(config);
|
|
5976
6119
|
}
|
|
5977
6120
|
}
|
|
5978
|
-
if (state2.isValidated)
|
|
5979
|
-
|
|
5980
|
-
validateNames();
|
|
5981
|
-
}
|
|
6121
|
+
if (state2.isValidated) validateNames();
|
|
6122
|
+
else syncVisualState();
|
|
5982
6123
|
return api;
|
|
5983
6124
|
};
|
|
5984
6125
|
|