@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -8
  3. package/dist/browser/global.d.ts +28 -18
  4. package/dist/browser/global.global.js +477 -66
  5. package/dist/browser/global.global.js.map +1 -1
  6. package/dist/browser/index.cjs +1450 -0
  7. package/dist/browser/index.cjs.map +1 -0
  8. package/dist/browser/index.d.cts +14 -0
  9. package/dist/browser/index.d.ts +14 -0
  10. package/dist/browser/index.js +23 -0
  11. package/dist/browser/index.js.map +1 -0
  12. package/dist/chunk-WGPAVJTK.js +1415 -0
  13. package/dist/chunk-WGPAVJTK.js.map +1 -0
  14. package/dist/index.cjs +296 -40
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +4 -158
  17. package/dist/index.d.ts +4 -158
  18. package/dist/index.js +7 -1148
  19. package/dist/index.js.map +1 -1
  20. package/dist/registry-GzopYPCR.d.cts +164 -0
  21. package/dist/registry-GzopYPCR.d.ts +164 -0
  22. package/docs/api/add-cleanup.md +52 -0
  23. package/docs/api/append.md +96 -0
  24. package/docs/api/auto-submit.md +91 -0
  25. package/docs/api/clear-errors.md +69 -0
  26. package/docs/api/destroy.md +80 -0
  27. package/docs/api/disable-auto-submit.md +65 -0
  28. package/docs/api/element.md +78 -0
  29. package/docs/api/form.md +156 -0
  30. package/docs/api/format.md +270 -0
  31. package/docs/api/get-data.md +90 -0
  32. package/docs/api/get-field.md +87 -0
  33. package/docs/api/get-state.md +111 -0
  34. package/docs/api/get-value.md +114 -0
  35. package/docs/api/index.md +55 -0
  36. package/docs/api/observe.md +126 -0
  37. package/docs/api/on-submit.md +94 -0
  38. package/docs/api/parse-form-data.md +94 -0
  39. package/docs/api/prefill.md +89 -0
  40. package/docs/api/regex.md +155 -0
  41. package/docs/api/reset.md +61 -0
  42. package/docs/api/revalidate.md +52 -0
  43. package/docs/api/set-errors.md +97 -0
  44. package/docs/api/set-value.md +120 -0
  45. package/docs/api/subscribe.md +107 -0
  46. package/docs/api/unwatch.md +74 -0
  47. package/docs/api/validate-field-value.md +144 -0
  48. package/docs/api/validate-values.md +103 -0
  49. package/docs/api/validate.md +102 -0
  50. package/docs/api/watch.md +117 -0
  51. package/docs/browser.md +156 -0
  52. package/docs/css-styling.md +191 -0
  53. package/docs/getting-started.md +222 -0
  54. package/docs/options.md +326 -0
  55. package/docs/recipes.md +530 -0
  56. package/docs/typescript.md +615 -0
  57. package/docs/vanilla.md +12 -0
  58. 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.querySelector(`.${classSelector}`) : null;
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
- const current = data[key];
487
- if (current === void 0) {
488
- data[key] = value;
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.querySelectorAll("input[name], select[name], textarea[name]")).filter(
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.querySelectorAll(`[${attribute}]`).forEach((node) => node.removeAttribute(attribute));
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
- clearAttributes(state2.element, [state2.attributes.error, state2.attributes.filled]);
748
- if (state2.isValidated) helpers.syncVisualState();
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
- const eventType = firstField instanceof HTMLSelectElement || firstField instanceof HTMLInputElement && (firstField.type === "checkbox" || firstField.type === "radio") ? "change" : "input";
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 validateFieldValue = (field, value, rules, values) => {
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 = { field, value, values };
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) && !pattern.value.test(toPatternTarget(value))) {
871
- errors.push(pattern.message ?? "Value does not match the required pattern.");
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
- if (!state2.fieldCache.has(name)) {
952
- state2.fieldCache.set(name, queryNamedFields(state2.element, name));
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 hasError = Boolean(errors[canonical]?.length);
987
- for (const field of fields) {
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 messages = validateFieldValue(name, values[name], rules, values);
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
- if (state2.isValidated && state2.validators[canonical]) {
1062
- validateNames([canonical]);
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) return;
1079
- const { data, formData } = state2.api.getData();
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
- handlers.forEach(
1082
- (handler) => handler.callback(state2.element, data, formData, snapshot)
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(state2.element, "change", handleDelegatedEvent);
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
- syncVisualState();
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;