@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.
Files changed (39) hide show
  1. package/README.md +6 -4
  2. package/dist/browser/global.d.ts +19 -3
  3. package/dist/browser/global.global.js +176 -30
  4. package/dist/browser/global.global.js.map +1 -1
  5. package/dist/browser/index.cjs +176 -30
  6. package/dist/browser/index.cjs.map +1 -1
  7. package/dist/browser/index.d.cts +1 -1
  8. package/dist/browser/index.d.ts +1 -1
  9. package/dist/browser/index.js +1 -1
  10. package/dist/{chunk-L4GRKSQ5.js → chunk-MMHX34WB.js} +181 -31
  11. package/dist/chunk-MMHX34WB.js.map +1 -0
  12. package/dist/index.cjs +180 -30
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +2 -2
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +1 -1
  17. package/dist/{registry-BpqJetAt.d.cts → registry-GzopYPCR.d.cts} +20 -4
  18. package/dist/{registry-BpqJetAt.d.ts → registry-GzopYPCR.d.ts} +20 -4
  19. package/docs/api/add-cleanup.md +52 -0
  20. package/docs/api/destroy.md +16 -1
  21. package/docs/api/form.md +4 -2
  22. package/docs/api/format.md +3 -3
  23. package/docs/api/get-state.md +17 -1
  24. package/docs/api/index.md +1 -0
  25. package/docs/api/on-submit.md +7 -3
  26. package/docs/api/reset.md +3 -2
  27. package/docs/api/revalidate.md +3 -1
  28. package/docs/api/subscribe.md +16 -0
  29. package/docs/api/validate-field-value.md +39 -3
  30. package/docs/api/validate-values.md +4 -1
  31. package/docs/api/validate.md +5 -3
  32. package/docs/browser.md +5 -5
  33. package/docs/css-styling.md +5 -1
  34. package/docs/getting-started.md +13 -8
  35. package/docs/options.md +69 -2
  36. package/docs/recipes.md +65 -11
  37. package/docs/typescript.md +47 -8
  38. package/package.json +1 -1
  39. 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.6.0/dist/browser/global.global.js"></script>
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.6.0` with the version you ship.
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.6.0/dist/browser/global.global.js"></script>
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
  ---
@@ -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 FieldValidationRules {
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
- validate?: FieldValidator | FieldValidator[];
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 = isMirror && shouldForceAutoInterpretation(formatOptions) ? buildAutoInterpretedOptions(formatOptions) : formatOptions;
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 { formatted, raw } = formatter.format(current, formatType, formatOptions);
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
- if (state2.isValidated) helpers.syncVisualState();
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 validateFieldValue = (field, value, rules, values) => {
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 = { field, value, values };
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 sameAs = resolveRule(rules.sameAs);
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 (sameAs.value !== void 0) {
5660
- const otherValue = values[sameAs.value];
5661
- if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
5662
- errors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
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 === void 0) continue;
5718
- const source = typeof sameAs === "string" ? sameAs : sameAs.value;
5719
- const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
5720
- dependents.add(field);
5721
- validationDependents.set(source, dependents);
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 hasError = Boolean(errors[canonical]?.length);
5776
- for (const field of fields) {
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 messages = validateFieldValue(name, values[name], rules, values);
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 field = getFieldsByName(displayName).find((candidate2) => {
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
- handlers.forEach(
5910
- (handler) => handler.callback(state2.element, data, formData, snapshot)
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
- syncVisualState();
5980
- validateNames();
5981
- }
6126
+ if (state2.isValidated) validateNames();
6127
+ else syncVisualState();
5982
6128
  return api;
5983
6129
  };
5984
6130