@samline/forms 2.7.2 → 2.9.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/CHANGELOG.md +316 -0
  2. package/README.md +119 -119
  3. package/dist/browser/global.d.ts +50 -16
  4. package/dist/browser/global.global.js +12 -6188
  5. package/dist/browser/global.global.js.map +1 -1
  6. package/dist/browser/index.cjs +340 -84
  7. package/dist/browser/index.cjs.map +1 -1
  8. package/dist/browser/index.d.cts +1 -1
  9. package/dist/browser/index.d.ts +1 -1
  10. package/dist/browser/index.js +1 -1
  11. package/dist/{chunk-MMHX34WB.js → chunk-6MA5TF6R.js} +341 -85
  12. package/dist/chunk-6MA5TF6R.js.map +1 -0
  13. package/dist/index.cjs +340 -84
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +2 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/{registry-GzopYPCR.d.ts → registry-CPe_lf8-.d.cts} +50 -4
  19. package/dist/{registry-GzopYPCR.d.cts → registry-CPe_lf8-.d.ts} +50 -4
  20. package/package.json +39 -8
  21. package/dist/chunk-MMHX34WB.js.map +0 -1
  22. package/docs/api/add-cleanup.md +0 -52
  23. package/docs/api/append.md +0 -96
  24. package/docs/api/auto-submit.md +0 -91
  25. package/docs/api/clear-errors.md +0 -69
  26. package/docs/api/destroy.md +0 -80
  27. package/docs/api/disable-auto-submit.md +0 -65
  28. package/docs/api/element.md +0 -78
  29. package/docs/api/form.md +0 -156
  30. package/docs/api/format.md +0 -270
  31. package/docs/api/get-data.md +0 -90
  32. package/docs/api/get-field.md +0 -87
  33. package/docs/api/get-state.md +0 -111
  34. package/docs/api/get-value.md +0 -114
  35. package/docs/api/index.md +0 -55
  36. package/docs/api/observe.md +0 -126
  37. package/docs/api/on-submit.md +0 -94
  38. package/docs/api/parse-form-data.md +0 -94
  39. package/docs/api/prefill.md +0 -89
  40. package/docs/api/regex.md +0 -155
  41. package/docs/api/reset.md +0 -61
  42. package/docs/api/revalidate.md +0 -52
  43. package/docs/api/set-errors.md +0 -97
  44. package/docs/api/set-value.md +0 -120
  45. package/docs/api/subscribe.md +0 -107
  46. package/docs/api/unwatch.md +0 -74
  47. package/docs/api/validate-field-value.md +0 -144
  48. package/docs/api/validate-values.md +0 -103
  49. package/docs/api/validate.md +0 -102
  50. package/docs/api/watch.md +0 -117
  51. package/docs/browser.md +0 -156
  52. package/docs/css-styling.md +0 -191
  53. package/docs/getting-started.md +0 -222
  54. package/docs/options.md +0 -326
  55. package/docs/recipes.md +0 -531
  56. package/docs/typescript.md +0 -615
  57. package/docs/vanilla.md +0 -12
  58. /package/{LICENSE → LICENSE.md} +0 -0
@@ -40,7 +40,7 @@ module.exports = __toCommonJS(browser_exports);
40
40
 
41
41
  // src/api/append.ts
42
42
  var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
43
- if (!state2.element) return null;
43
+ if (!state2.element || state2.isDestroyed) return null;
44
44
  helpers.clearFieldCache();
45
45
  if (className) {
46
46
  const classSelector = className.trim().split(/\s+/)[0];
@@ -76,6 +76,7 @@ var createAddCleanup = (state2, _helpers) => (cleanup) => {
76
76
 
77
77
  // src/api/auto-submit.ts
78
78
  var createAutoSubmit = (state2, helpers) => (next = true) => {
79
+ if (state2.isDestroyed) return state2.api;
79
80
  state2.autoSubmitEnabled = next !== false;
80
81
  if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
81
82
  clearTimeout(state2.autoSubmitTimer);
@@ -88,6 +89,7 @@ var createAutoSubmit = (state2, helpers) => (next = true) => {
88
89
 
89
90
  // src/api/clear-errors.ts
90
91
  var createClearErrors = (state2, helpers) => (fields) => {
92
+ if (state2.isDestroyed) return state2.api;
91
93
  if (!fields) {
92
94
  state2.manualErrors = {};
93
95
  helpers.syncVisualState();
@@ -214,6 +216,7 @@ var isFormatChar = (char) => {
214
216
  return true;
215
217
  };
216
218
  var restoreCursor = (field, position) => {
219
+ if (field.ownerDocument.activeElement !== field) return;
217
220
  const max = field.value.length;
218
221
  const safe = clamp(position, 0, max);
219
222
  const apply = () => {
@@ -516,6 +519,7 @@ var createDestroy = (state2, _helpers) => () => {
516
519
  }
517
520
  }
518
521
  state2.watchedFields.clear();
522
+ state2.groupWatchers.clear();
519
523
  state2.subscribers.clear();
520
524
  state2.submitHandlers.clear();
521
525
  state2.fieldCache.clear();
@@ -608,7 +612,7 @@ var mergeErrors = (left, right) => {
608
612
  var createGetState = (state2, helpers) => () => {
609
613
  const values = helpers.getValues();
610
614
  const errors = cloneErrors(helpers.getMergedErrors());
611
- const filledFields = helpers.getTrackedFieldNames().filter((name) => {
615
+ const filledFields = Object.keys(values).filter((name) => {
612
616
  const value = values[name];
613
617
  if (Array.isArray(value)) return value.length > 0;
614
618
  if (typeof value === "string") return value.trim().length > 0;
@@ -743,9 +747,9 @@ var writeFieldValue = (fields, value) => {
743
747
  continue;
744
748
  }
745
749
  if (field instanceof HTMLSelectElement && field.multiple) {
746
- const selectedValues = normalizedArray ?? [normalizedValue];
750
+ const selectedValues2 = normalizedArray ?? [normalizedValue];
747
751
  for (const option of field.options) {
748
- option.selected = selectedValues.includes(option.value);
752
+ option.selected = selectedValues2.includes(option.value);
749
753
  }
750
754
  continue;
751
755
  }
@@ -780,7 +784,7 @@ var submitForm = (form3) => {
780
784
  var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
781
785
 
782
786
  // src/api/observe.ts
783
- var createObserve = (state2, helpers) => (field, callback) => {
787
+ var observeField = (state2, field, callback) => {
784
788
  const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
785
789
  callbacks.add(callback);
786
790
  state2.watchedFields.set(field, callbacks);
@@ -795,47 +799,72 @@ var createObserve = (state2, helpers) => (field, callback) => {
795
799
  }
796
800
  };
797
801
  };
802
+ var observeFields = (state2, fields, callback) => {
803
+ const names = Array.from(new Set(fields));
804
+ if (names.length === 0) return () => void 0;
805
+ const entry = { fields: names, callback };
806
+ state2.groupWatchers.add(entry);
807
+ if (state2.element) {
808
+ const values = Object.fromEntries(names.map((name) => [name, state2.api.getValue(name)]));
809
+ callback(values, null, state2.element, state2.api.getState());
810
+ }
811
+ return () => {
812
+ state2.groupWatchers.delete(entry);
813
+ };
814
+ };
815
+ var createObserve = (state2, _helpers) => {
816
+ function observe(field, callback) {
817
+ if (state2.isDestroyed) return () => void 0;
818
+ return Array.isArray(field) ? observeFields(state2, field, callback) : observeField(state2, field, callback);
819
+ }
820
+ return observe;
821
+ };
798
822
 
799
823
  // src/api/on-submit.ts
800
824
  var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
825
+ if (state2.isDestroyed) return state2.api;
801
826
  state2.submitHandlers.add({ callback, preventDefault });
802
827
  return state2.api;
803
828
  };
804
829
 
805
830
  // src/api/prefill.ts
806
- var createPrefill = (state2, _helpers) => (fieldName) => {
807
- if (!state2.element) return state2.api;
831
+ var createPrefill = (state2, helpers) => (fieldName) => {
832
+ if (!state2.element || state2.isDestroyed) return state2.api;
808
833
  const params = new URLSearchParams(window.location.search);
809
- params.forEach((value, key) => {
810
- if (fieldName && fieldName !== key) return;
811
- state2.api.setValue(key, value);
812
- });
834
+ const keys = Array.from(new Set(params.keys())).filter(
835
+ (key) => !fieldName || fieldName === key
836
+ );
837
+ if (keys.length === 0) return state2.api;
838
+ state2.autoSubmitSuspended = true;
839
+ try {
840
+ for (const key of keys) {
841
+ const values = params.getAll(key);
842
+ state2.api.setValue(key, values.length > 1 ? values : values[0]);
843
+ }
844
+ } finally {
845
+ state2.autoSubmitSuspended = false;
846
+ }
847
+ helpers.scheduleAutoSubmit();
813
848
  return state2.api;
814
849
  };
815
850
 
816
851
  // src/api/reset.ts
817
852
  var createReset = (state2, helpers) => () => {
818
- if (!state2.element) return state2.api;
819
- state2.element.reset();
820
- resetFormattedFields(state2);
821
- state2.manualErrors = {};
822
- state2.validationErrors = {};
823
- state2.validationGroupErrors = {};
824
- state2.validationElementErrors.clear();
825
- clearAttributes(state2.element, [
826
- state2.attributes.error,
827
- state2.attributes.filled,
828
- "aria-invalid"
829
- ]);
830
- helpers.syncVisualState();
831
- helpers.notifySubscribers();
853
+ if (!state2.element || state2.isDestroyed) return state2.api;
854
+ state2.isResetting = true;
855
+ try {
856
+ state2.element.reset();
857
+ } finally {
858
+ state2.isResetting = false;
859
+ }
860
+ helpers.applyReset();
832
861
  return state2.api;
833
862
  };
834
863
 
835
864
  // src/api/validate.ts
836
865
  var createValidate = (state2, helpers) => (fields) => {
866
+ if (state2.isDestroyed) return { isValid: true, errors: {} };
837
867
  state2.isValidated = true;
838
- helpers.syncVisualState(fields);
839
868
  return helpers.validateNames(fields);
840
869
  };
841
870
 
@@ -844,6 +873,7 @@ var createRevalidate = createValidate;
844
873
 
845
874
  // src/api/set-errors.ts
846
875
  var createSetErrors = (state2, helpers) => (fields) => {
876
+ if (state2.isDestroyed) return state2.api;
847
877
  if (Array.isArray(fields)) {
848
878
  state2.manualErrors = {
849
879
  ...state2.manualErrors,
@@ -860,8 +890,78 @@ var createSetErrors = (state2, helpers) => (fields) => {
860
890
  return state2.api;
861
891
  };
862
892
 
893
+ // src/api/set-options.ts
894
+ var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.options);
895
+ var createOption = (entry) => {
896
+ const option = document.createElement("option");
897
+ if (typeof entry === "object") {
898
+ option.value = String(entry.value);
899
+ option.textContent = entry.label ?? String(entry.value);
900
+ option.disabled = Boolean(entry.disabled);
901
+ if (entry.selected) option.defaultSelected = true;
902
+ } else {
903
+ option.value = String(entry);
904
+ option.textContent = String(entry);
905
+ }
906
+ return option;
907
+ };
908
+ var buildNodes = (options) => {
909
+ const nodes = [];
910
+ let hasExplicitSelection = false;
911
+ const track = (option) => {
912
+ if (option.defaultSelected) hasExplicitSelection = true;
913
+ return option;
914
+ };
915
+ for (const entry of options) {
916
+ if (entry === null || entry === void 0) continue;
917
+ if (isGroup(entry)) {
918
+ const group = document.createElement("optgroup");
919
+ group.label = entry.label;
920
+ group.disabled = Boolean(entry.disabled);
921
+ for (const child of entry.options) {
922
+ if (child === null || child === void 0) continue;
923
+ group.appendChild(track(createOption(child)));
924
+ }
925
+ nodes.push(group);
926
+ continue;
927
+ }
928
+ nodes.push(track(createOption(entry)));
929
+ }
930
+ return { nodes, hasExplicitSelection };
931
+ };
932
+ var selectedValues = (select) => Array.from(select.selectedOptions, (option) => option.value);
933
+ var restoreSelection = (select, previous) => {
934
+ if (previous.length === 0) return;
935
+ const options = Array.from(select.options);
936
+ if (select.multiple) {
937
+ for (const option of options) option.selected = previous.includes(option.value);
938
+ return;
939
+ }
940
+ const match = options.find((option) => option.value === previous[0]);
941
+ if (match) match.selected = true;
942
+ };
943
+ var sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
944
+ var createSetOptions = (state2, helpers) => (name, options, config = {}) => {
945
+ if (!state2.element || state2.isDestroyed) return state2.api;
946
+ const selects = helpers.getFieldsByName(name).filter((field) => field instanceof HTMLSelectElement);
947
+ if (selects.length === 0) return state2.api;
948
+ const keepValue = config.keepValue ?? true;
949
+ const changed = [];
950
+ for (const select of selects) {
951
+ const previous = selectedValues(select);
952
+ const { nodes, hasExplicitSelection } = buildNodes(options);
953
+ select.replaceChildren(...nodes);
954
+ if (keepValue && !hasExplicitSelection) restoreSelection(select, previous);
955
+ if (!sameValues(previous, selectedValues(select))) changed.push(select);
956
+ }
957
+ helpers.commitOwnMutations(selects);
958
+ changed[0]?.dispatchEvent(new Event("input", { bubbles: true }));
959
+ return state2.api;
960
+ };
961
+
863
962
  // src/api/set-value.ts
864
963
  var createSetValue = (state2, helpers) => (name, value) => {
964
+ if (state2.isDestroyed) return state2.api;
865
965
  const fields = helpers.getFieldsByName(name);
866
966
  if (fields.length === 0) return state2.api;
867
967
  writeFieldValue(fields, value);
@@ -873,6 +973,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
873
973
 
874
974
  // src/api/subscribe.ts
875
975
  var createSubscribe = (state2, _helpers) => (listener) => {
976
+ if (state2.isDestroyed) return () => void 0;
876
977
  state2.subscribers.add(listener);
877
978
  listener(state2.api.getState());
878
979
  return () => {
@@ -881,28 +982,46 @@ var createSubscribe = (state2, _helpers) => (listener) => {
881
982
  };
882
983
 
883
984
  // src/api/unwatch.ts
884
- var createUnwatch = (state2, _helpers) => (field, callback) => {
885
- if (!field) {
886
- state2.watchedFields.clear();
887
- return state2.api;
888
- }
889
- if (!callback) {
890
- state2.watchedFields.delete(field);
985
+ var sameFields = (left, right) => left.length === right.size && left.every((name) => right.has(name));
986
+ var createUnwatch = (state2, _helpers) => {
987
+ function unwatch(field, callback) {
988
+ if (field === void 0 || field === "") {
989
+ state2.watchedFields.clear();
990
+ state2.groupWatchers.clear();
991
+ return state2.api;
992
+ }
993
+ if (Array.isArray(field)) {
994
+ const names = new Set(field);
995
+ for (const entry of Array.from(state2.groupWatchers)) {
996
+ if (!sameFields(entry.fields, names)) continue;
997
+ if (callback && entry.callback !== callback) continue;
998
+ state2.groupWatchers.delete(entry);
999
+ }
1000
+ return state2.api;
1001
+ }
1002
+ if (!callback) {
1003
+ state2.watchedFields.delete(field);
1004
+ return state2.api;
1005
+ }
1006
+ const callbacks = state2.watchedFields.get(field);
1007
+ callbacks?.delete(callback);
1008
+ if (callbacks && callbacks.size === 0) {
1009
+ state2.watchedFields.delete(field);
1010
+ }
891
1011
  return state2.api;
892
1012
  }
893
- const callbacks = state2.watchedFields.get(field);
894
- callbacks?.delete(callback);
895
- if (callbacks && callbacks.size === 0) {
896
- state2.watchedFields.delete(field);
897
- }
898
- return state2.api;
1013
+ return unwatch;
899
1014
  };
900
1015
 
901
1016
  // src/api/watch.ts
902
- var createWatch = (state2, helpers) => (field, callback) => {
1017
+ var createWatch = (state2, helpers) => {
903
1018
  const observe = createObserve(state2, helpers);
904
- observe(field, callback);
905
- return state2.api;
1019
+ function watch(field, callback) {
1020
+ if (Array.isArray(field)) observe(field, callback);
1021
+ else observe(field, callback);
1022
+ return state2.api;
1023
+ }
1024
+ return watch;
906
1025
  };
907
1026
 
908
1027
  // src/core/validation.ts
@@ -1055,6 +1174,7 @@ var createFormController = (target, options = {}) => {
1055
1174
  attributes,
1056
1175
  validators: normalizedOptions.validators ?? {},
1057
1176
  watchedFields: /* @__PURE__ */ new Map(),
1177
+ groupWatchers: /* @__PURE__ */ new Set(),
1058
1178
  subscribers: /* @__PURE__ */ new Set(),
1059
1179
  submitHandlers: /* @__PURE__ */ new Set(),
1060
1180
  fieldCache: /* @__PURE__ */ new Map(),
@@ -1068,6 +1188,9 @@ var createFormController = (target, options = {}) => {
1068
1188
  submitCount: 0,
1069
1189
  pendingSubmitCount: 0,
1070
1190
  autoSubmitTimer: null,
1191
+ isAutoSubmitting: false,
1192
+ autoSubmitSuspended: false,
1193
+ isResetting: false,
1071
1194
  isDestroyed: false,
1072
1195
  listeners: [],
1073
1196
  mutationObserver: null,
@@ -1112,12 +1235,20 @@ var createFormController = (target, options = {}) => {
1112
1235
  state2.fieldCache.set(name, fields);
1113
1236
  return fields;
1114
1237
  };
1115
- const getTrackedFieldNames = () => {
1116
- if (!state2.element) return Object.keys(state2.validators);
1117
- const names = new Set(Object.keys(state2.validators));
1238
+ const indexFieldsByName = () => {
1239
+ const index = /* @__PURE__ */ new Map();
1240
+ if (!state2.element) return index;
1118
1241
  for (const field of getNamedFields(state2.element)) {
1119
- names.add(field.name);
1242
+ const bucket = index.get(field.name);
1243
+ if (bucket) bucket.push(field);
1244
+ else index.set(field.name, [field]);
1120
1245
  }
1246
+ return index;
1247
+ };
1248
+ const getTrackedFieldNames = (index = indexFieldsByName()) => {
1249
+ if (!state2.element) return Object.keys(state2.validators);
1250
+ const names = new Set(Object.keys(state2.validators));
1251
+ for (const name of index.keys()) names.add(name);
1121
1252
  for (const [canonical, entry] of state2.formattedFields) {
1122
1253
  names.add(canonical);
1123
1254
  if (entry.displayName && entry.displayName !== canonical) {
@@ -1128,8 +1259,9 @@ var createFormController = (target, options = {}) => {
1128
1259
  };
1129
1260
  const getValues = () => {
1130
1261
  const values = {};
1131
- for (const name of getTrackedFieldNames()) {
1132
- values[name] = readFieldValue(getFieldsByName(name));
1262
+ const index = indexFieldsByName();
1263
+ for (const name of getTrackedFieldNames(index)) {
1264
+ values[name] = readFieldValue(index.get(name) ?? []);
1133
1265
  }
1134
1266
  return values;
1135
1267
  };
@@ -1213,7 +1345,7 @@ var createFormController = (target, options = {}) => {
1213
1345
  }
1214
1346
  return affected;
1215
1347
  };
1216
- const emitFieldWatchers = (name) => {
1348
+ const emitSingleWatchers = (name) => {
1217
1349
  if (!state2.element || !state2.api) return;
1218
1350
  const callbacks = state2.watchedFields.get(name);
1219
1351
  if (!callbacks || callbacks.size === 0) return;
@@ -1222,29 +1354,85 @@ var createFormController = (target, options = {}) => {
1222
1354
  const snapshot = state2.api.getState();
1223
1355
  callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1224
1356
  };
1357
+ const emitGroupWatchers = (names) => {
1358
+ if (!state2.element || !state2.api || state2.groupWatchers.size === 0) return;
1359
+ let snapshot;
1360
+ for (const entry of Array.from(state2.groupWatchers)) {
1361
+ const changed = names ? names.find((name) => entry.fields.includes(name)) : null;
1362
+ if (changed === void 0) continue;
1363
+ snapshot ?? (snapshot = state2.api.getState());
1364
+ const values = Object.fromEntries(
1365
+ entry.fields.map((field) => [field, state2.api.getValue(field)])
1366
+ );
1367
+ entry.callback(values, changed, state2.element, snapshot);
1368
+ }
1369
+ };
1370
+ const emitFieldWatchers = (name) => {
1371
+ emitSingleWatchers(name);
1372
+ emitGroupWatchers([name]);
1373
+ };
1374
+ const runAutoSubmit = () => {
1375
+ state2.autoSubmitTimer = null;
1376
+ if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
1377
+ state2.isAutoSubmitting = true;
1378
+ try {
1379
+ submitForm(state2.element);
1380
+ } finally {
1381
+ state2.isAutoSubmitting = false;
1382
+ }
1383
+ };
1225
1384
  const scheduleAutoSubmit = () => {
1226
- if (!state2.element || !state2.autoSubmitEnabled) return;
1385
+ if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
1227
1386
  if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
1387
+ state2.autoSubmitTimer = null;
1228
1388
  if (state2.autoSubmitDebounce > 0) {
1229
- state2.autoSubmitTimer = setTimeout(
1230
- () => submitForm(state2.element),
1231
- state2.autoSubmitDebounce
1232
- );
1389
+ state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
1233
1390
  return;
1234
1391
  }
1235
- submitForm(state2.element);
1392
+ runAutoSubmit();
1393
+ };
1394
+ const selectSnapshots = /* @__PURE__ */ new WeakMap();
1395
+ const readSelectSnapshot = (select) => Array.from(select.selectedOptions, (option) => option.value).join("\0");
1396
+ const snapshotSelect = (select) => {
1397
+ selectSnapshots.set(select, readSelectSnapshot(select));
1398
+ };
1399
+ const snapshotAllSelects = () => {
1400
+ if (!state2.element) return;
1401
+ for (const field of getNamedFields(state2.element)) {
1402
+ if (field instanceof HTMLSelectElement) snapshotSelect(field);
1403
+ }
1404
+ };
1405
+ const emitAllFieldWatchers = () => {
1406
+ for (const name of Array.from(state2.watchedFields.keys())) emitSingleWatchers(name);
1407
+ emitGroupWatchers(null);
1408
+ };
1409
+ const applyReset = () => {
1410
+ if (!state2.element || state2.isDestroyed) return;
1411
+ clearFieldCache();
1412
+ resetFormattedFields(state2);
1413
+ state2.manualErrors = {};
1414
+ state2.validationErrors = {};
1415
+ state2.validationGroupErrors = {};
1416
+ state2.validationElementErrors.clear();
1417
+ clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
1418
+ syncVisualState();
1419
+ snapshotAllSelects();
1420
+ emitAllFieldWatchers();
1421
+ notifySubscribers();
1236
1422
  };
1237
1423
  const helpers = {
1238
1424
  notifySubscribers,
1239
1425
  clearFieldCache,
1240
1426
  getFieldsByName,
1241
- getTrackedFieldNames,
1427
+ getTrackedFieldNames: () => getTrackedFieldNames(),
1242
1428
  getValues,
1243
1429
  getMergedErrors,
1244
1430
  syncVisualState,
1245
1431
  validateNames,
1246
1432
  emitFieldWatchers,
1247
- scheduleAutoSubmit
1433
+ scheduleAutoSubmit,
1434
+ applyReset,
1435
+ commitOwnMutations: (selects) => commitOwnMutations(selects)
1248
1436
  };
1249
1437
  const handleDelegatedEvent = (event) => {
1250
1438
  const target2 = event.target;
@@ -1252,6 +1440,7 @@ var createFormController = (target, options = {}) => {
1252
1440
  const name = target2.name;
1253
1441
  if (!name) return;
1254
1442
  clearFieldCache();
1443
+ if (target2 instanceof HTMLSelectElement) snapshotSelect(target2);
1255
1444
  const canonical = resolveCanonicalForName(state2, name) ?? name;
1256
1445
  const displayName = resolveDisplayNameForName(state2, name);
1257
1446
  if (normalizedOptions.clearManualErrorsOnChange) {
@@ -1262,8 +1451,9 @@ var createFormController = (target, options = {}) => {
1262
1451
  if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
1263
1452
  validateNames(affectedValidationNames);
1264
1453
  }
1265
- emitFieldWatchers(canonical);
1266
- if (canonical !== name) emitFieldWatchers(name);
1454
+ emitSingleWatchers(canonical);
1455
+ if (canonical !== name) emitSingleWatchers(name);
1456
+ emitGroupWatchers(canonical !== name ? [canonical, name] : [canonical]);
1267
1457
  notifySubscribers();
1268
1458
  scheduleAutoSubmit();
1269
1459
  };
@@ -1273,6 +1463,23 @@ var createFormController = (target, options = {}) => {
1273
1463
  if (target2.form !== state2.element || state2.element?.contains(target2)) return;
1274
1464
  handleDelegatedEvent(event);
1275
1465
  };
1466
+ const isFocusable = (candidate) => {
1467
+ if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
1468
+ return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
1469
+ };
1470
+ const focusFirstInvalidField = (errors) => {
1471
+ const candidates = [];
1472
+ for (const name of Object.keys(errors)) {
1473
+ const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
1474
+ const elementErrors = state2.validationElementErrors.get(name);
1475
+ const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
1476
+ if (field) candidates.push(field);
1477
+ }
1478
+ candidates.sort(
1479
+ (left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
1480
+ );
1481
+ candidates[0]?.focus();
1482
+ };
1276
1483
  const handleSubmitEvent = (event) => {
1277
1484
  if (!state2.element || state2.isDestroyed || !state2.api) return;
1278
1485
  if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
@@ -1283,23 +1490,7 @@ var createFormController = (target, options = {}) => {
1283
1490
  state2.submitCount += 1;
1284
1491
  notifySubscribers();
1285
1492
  if (!validation.isValid) {
1286
- for (const name of Object.keys(validation.errors)) {
1287
- const displayName = resolveDisplayNameForName(state2, name);
1288
- const fields = getFieldsByName(displayName);
1289
- const elementErrors = state2.validationElementErrors.get(name);
1290
- const field = fields.find((candidate2) => {
1291
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1292
- if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
1293
- return elementErrors?.has(candidate2);
1294
- }) ?? fields.find((candidate2) => {
1295
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1296
- return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
1297
- });
1298
- if (field) {
1299
- field.focus();
1300
- break;
1301
- }
1302
- }
1493
+ if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
1303
1494
  return;
1304
1495
  }
1305
1496
  const candidate = event;
@@ -1312,7 +1503,13 @@ var createFormController = (target, options = {}) => {
1312
1503
  try {
1313
1504
  for (const handler of handlers) {
1314
1505
  const result = handler.callback(state2.element, data, formData, snapshot);
1315
- if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
1506
+ if (result && typeof result.then === "function") {
1507
+ pending.push(
1508
+ Promise.resolve(result).catch((error) => {
1509
+ console.error("[forms] submit handler failed", error);
1510
+ })
1511
+ );
1512
+ }
1316
1513
  }
1317
1514
  } catch (error) {
1318
1515
  synchronousError = error;
@@ -1328,15 +1525,71 @@ var createFormController = (target, options = {}) => {
1328
1525
  }
1329
1526
  if (hasSynchronousError) throw synchronousError;
1330
1527
  };
1528
+ const handleResetEvent = (event) => {
1529
+ if (state2.isResetting) return;
1530
+ const timer = setTimeout(() => {
1531
+ state2.cleanups.delete(cancel);
1532
+ if (!event.defaultPrevented) applyReset();
1533
+ }, 0);
1534
+ const cancel = () => clearTimeout(timer);
1535
+ state2.cleanups.add(cancel);
1536
+ };
1537
+ const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1538
+ const isStructural = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1539
+ const isOptionNode = (node) => node instanceof HTMLOptionElement || node instanceof HTMLOptGroupElement;
1540
+ const optionListOwner = (record) => {
1541
+ if (record.type !== "childList") return null;
1542
+ const touchesOptions = Array.from(record.addedNodes).some(isOptionNode) || Array.from(record.removedNodes).some(isOptionNode);
1543
+ if (!touchesOptions) return null;
1544
+ const target2 = record.target;
1545
+ if (target2 instanceof HTMLSelectElement) return target2;
1546
+ if (target2 instanceof HTMLOptGroupElement && target2.parentElement instanceof HTMLSelectElement) {
1547
+ return target2.parentElement;
1548
+ }
1549
+ return null;
1550
+ };
1551
+ const handleMutations = (records) => {
1552
+ if (state2.isDestroyed) return;
1553
+ const structural = records.some(isStructural);
1554
+ const changedNames = /* @__PURE__ */ new Set();
1555
+ for (const record of records) {
1556
+ const select = optionListOwner(record);
1557
+ if (!select || !select.name) continue;
1558
+ const next = readSelectSnapshot(select);
1559
+ if (selectSnapshots.get(select) !== next) changedNames.add(select.name);
1560
+ selectSnapshots.set(select, next);
1561
+ }
1562
+ if (!structural && changedNames.size === 0) return;
1563
+ clearFieldCache();
1564
+ if (structural) {
1565
+ snapshotAllSelects();
1566
+ if (state2.isValidated) validateNames();
1567
+ else syncVisualState();
1568
+ } else {
1569
+ const affected = /* @__PURE__ */ new Set();
1570
+ for (const name of changedNames) {
1571
+ for (const field of getAffectedValidationNames(name)) affected.add(field);
1572
+ }
1573
+ const names = Array.from(affected);
1574
+ syncVisualState(Array.from(changedNames));
1575
+ if (state2.isValidated && names.some((field) => state2.validators[field] !== void 0)) {
1576
+ validateNames(names);
1577
+ }
1578
+ }
1579
+ for (const name of changedNames) emitFieldWatchers(name);
1580
+ notifySubscribers();
1581
+ };
1582
+ const commitOwnMutations = (selects) => {
1583
+ selects.forEach(snapshotSelect);
1584
+ const pending = state2.mutationObserver?.takeRecords() ?? [];
1585
+ const foreign = pending.filter(
1586
+ (record) => !selects.some((select) => select.contains(record.target))
1587
+ );
1588
+ if (foreign.length > 0) handleMutations(foreign);
1589
+ };
1331
1590
  const startMutationObserver = () => {
1332
1591
  if (!state2.element || typeof MutationObserver === "undefined") return;
1333
- state2.mutationObserver = new MutationObserver(() => {
1334
- clearFieldCache();
1335
- if (state2.isValidated) {
1336
- validateNames();
1337
- } else syncVisualState();
1338
- notifySubscribers();
1339
- });
1592
+ state2.mutationObserver = new MutationObserver(handleMutations);
1340
1593
  state2.mutationObserver.observe(state2.element, {
1341
1594
  childList: true,
1342
1595
  subtree: true,
@@ -1365,6 +1618,7 @@ var createFormController = (target, options = {}) => {
1365
1618
  setErrors: createSetErrors(state2, helpers),
1366
1619
  clearErrors: createClearErrors(state2, helpers),
1367
1620
  setValue: createSetValue(state2, helpers),
1621
+ setOptions: createSetOptions(state2, helpers),
1368
1622
  getValue: createGetValue(state2, helpers),
1369
1623
  getField: createGetField(state2, helpers),
1370
1624
  validate: createValidate(state2, helpers),
@@ -1383,6 +1637,8 @@ var createFormController = (target, options = {}) => {
1383
1637
  addListener2(state2.element, "input", handleDelegatedEvent);
1384
1638
  addListener2(document, "input", handleExternalDelegatedEvent);
1385
1639
  addListener2(state2.element, "submit", handleSubmitEvent);
1640
+ addListener2(state2.element, "reset", handleResetEvent);
1641
+ snapshotAllSelects();
1386
1642
  startMutationObserver();
1387
1643
  }
1388
1644
  if (normalizedOptions.autoSubmit) {