@samline/forms 2.7.2 → 2.8.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 (52) hide show
  1. package/CHANGELOG.md +302 -0
  2. package/README.md +110 -122
  3. package/dist/browser/global.d.ts +1 -13
  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 +125 -56
  7. package/dist/browser/index.cjs.map +1 -1
  8. package/dist/browser/index.js +1 -1
  9. package/dist/{chunk-MMHX34WB.js → chunk-23VZAYA3.js} +126 -57
  10. package/dist/chunk-23VZAYA3.js.map +1 -0
  11. package/dist/index.cjs +125 -56
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/package.json +35 -7
  15. package/dist/chunk-MMHX34WB.js.map +0 -1
  16. package/docs/api/add-cleanup.md +0 -52
  17. package/docs/api/append.md +0 -96
  18. package/docs/api/auto-submit.md +0 -91
  19. package/docs/api/clear-errors.md +0 -69
  20. package/docs/api/destroy.md +0 -80
  21. package/docs/api/disable-auto-submit.md +0 -65
  22. package/docs/api/element.md +0 -78
  23. package/docs/api/form.md +0 -156
  24. package/docs/api/format.md +0 -270
  25. package/docs/api/get-data.md +0 -90
  26. package/docs/api/get-field.md +0 -87
  27. package/docs/api/get-state.md +0 -111
  28. package/docs/api/get-value.md +0 -114
  29. package/docs/api/index.md +0 -55
  30. package/docs/api/observe.md +0 -126
  31. package/docs/api/on-submit.md +0 -94
  32. package/docs/api/parse-form-data.md +0 -94
  33. package/docs/api/prefill.md +0 -89
  34. package/docs/api/regex.md +0 -155
  35. package/docs/api/reset.md +0 -61
  36. package/docs/api/revalidate.md +0 -52
  37. package/docs/api/set-errors.md +0 -97
  38. package/docs/api/set-value.md +0 -120
  39. package/docs/api/subscribe.md +0 -107
  40. package/docs/api/unwatch.md +0 -74
  41. package/docs/api/validate-field-value.md +0 -144
  42. package/docs/api/validate-values.md +0 -103
  43. package/docs/api/validate.md +0 -102
  44. package/docs/api/watch.md +0 -117
  45. package/docs/browser.md +0 -156
  46. package/docs/css-styling.md +0 -191
  47. package/docs/getting-started.md +0 -222
  48. package/docs/options.md +0 -326
  49. package/docs/recipes.md +0 -531
  50. package/docs/typescript.md +0 -615
  51. package/docs/vanilla.md +0 -12
  52. /package/{LICENSE → LICENSE.md} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  browser
3
- } from "../chunk-MMHX34WB.js";
3
+ } from "../chunk-23VZAYA3.js";
4
4
 
5
5
  // src/browser/global.ts
6
6
  var Forms = browser;
@@ -172,7 +172,7 @@ var validateValues = (values, schema) => {
172
172
 
173
173
  // src/api/append.ts
174
174
  var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
175
- if (!state2.element) return null;
175
+ if (!state2.element || state2.isDestroyed) return null;
176
176
  helpers.clearFieldCache();
177
177
  if (className) {
178
178
  const classSelector = className.trim().split(/\s+/)[0];
@@ -208,6 +208,7 @@ var createAddCleanup = (state2, _helpers) => (cleanup) => {
208
208
 
209
209
  // src/api/auto-submit.ts
210
210
  var createAutoSubmit = (state2, helpers) => (next = true) => {
211
+ if (state2.isDestroyed) return state2.api;
211
212
  state2.autoSubmitEnabled = next !== false;
212
213
  if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
213
214
  clearTimeout(state2.autoSubmitTimer);
@@ -220,6 +221,7 @@ var createAutoSubmit = (state2, helpers) => (next = true) => {
220
221
 
221
222
  // src/api/clear-errors.ts
222
223
  var createClearErrors = (state2, helpers) => (fields) => {
224
+ if (state2.isDestroyed) return state2.api;
223
225
  if (!fields) {
224
226
  state2.manualErrors = {};
225
227
  helpers.syncVisualState();
@@ -346,6 +348,7 @@ var isFormatChar = (char) => {
346
348
  return true;
347
349
  };
348
350
  var restoreCursor = (field, position) => {
351
+ if (field.ownerDocument.activeElement !== field) return;
349
352
  const max = field.value.length;
350
353
  const safe = clamp(position, 0, max);
351
354
  const apply = () => {
@@ -709,7 +712,7 @@ var mergeErrors = (left, right) => {
709
712
  var createGetState = (state2, helpers) => () => {
710
713
  const values = helpers.getValues();
711
714
  const errors = cloneErrors(helpers.getMergedErrors());
712
- const filledFields = helpers.getTrackedFieldNames().filter((name) => {
715
+ const filledFields = Object.keys(values).filter((name) => {
713
716
  const value = values[name];
714
717
  if (Array.isArray(value)) return value.length > 0;
715
718
  if (typeof value === "string") return value.trim().length > 0;
@@ -882,6 +885,7 @@ var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFi
882
885
 
883
886
  // src/api/observe.ts
884
887
  var createObserve = (state2, helpers) => (field, callback) => {
888
+ if (state2.isDestroyed) return () => void 0;
885
889
  const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
886
890
  callbacks.add(callback);
887
891
  state2.watchedFields.set(field, callbacks);
@@ -899,44 +903,49 @@ var createObserve = (state2, helpers) => (field, callback) => {
899
903
 
900
904
  // src/api/on-submit.ts
901
905
  var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
906
+ if (state2.isDestroyed) return state2.api;
902
907
  state2.submitHandlers.add({ callback, preventDefault });
903
908
  return state2.api;
904
909
  };
905
910
 
906
911
  // src/api/prefill.ts
907
- var createPrefill = (state2, _helpers) => (fieldName) => {
908
- if (!state2.element) return state2.api;
912
+ var createPrefill = (state2, helpers) => (fieldName) => {
913
+ if (!state2.element || state2.isDestroyed) return state2.api;
909
914
  const params = new URLSearchParams(window.location.search);
910
- params.forEach((value, key) => {
911
- if (fieldName && fieldName !== key) return;
912
- state2.api.setValue(key, value);
913
- });
915
+ const keys = Array.from(new Set(params.keys())).filter(
916
+ (key) => !fieldName || fieldName === key
917
+ );
918
+ if (keys.length === 0) return state2.api;
919
+ state2.autoSubmitSuspended = true;
920
+ try {
921
+ for (const key of keys) {
922
+ const values = params.getAll(key);
923
+ state2.api.setValue(key, values.length > 1 ? values : values[0]);
924
+ }
925
+ } finally {
926
+ state2.autoSubmitSuspended = false;
927
+ }
928
+ helpers.scheduleAutoSubmit();
914
929
  return state2.api;
915
930
  };
916
931
 
917
932
  // src/api/reset.ts
918
933
  var createReset = (state2, helpers) => () => {
919
- if (!state2.element) return state2.api;
920
- state2.element.reset();
921
- resetFormattedFields(state2);
922
- state2.manualErrors = {};
923
- state2.validationErrors = {};
924
- state2.validationGroupErrors = {};
925
- state2.validationElementErrors.clear();
926
- clearAttributes(state2.element, [
927
- state2.attributes.error,
928
- state2.attributes.filled,
929
- "aria-invalid"
930
- ]);
931
- helpers.syncVisualState();
932
- helpers.notifySubscribers();
934
+ if (!state2.element || state2.isDestroyed) return state2.api;
935
+ state2.isResetting = true;
936
+ try {
937
+ state2.element.reset();
938
+ } finally {
939
+ state2.isResetting = false;
940
+ }
941
+ helpers.applyReset();
933
942
  return state2.api;
934
943
  };
935
944
 
936
945
  // src/api/validate.ts
937
946
  var createValidate = (state2, helpers) => (fields) => {
947
+ if (state2.isDestroyed) return { isValid: true, errors: {} };
938
948
  state2.isValidated = true;
939
- helpers.syncVisualState(fields);
940
949
  return helpers.validateNames(fields);
941
950
  };
942
951
 
@@ -945,6 +954,7 @@ var createRevalidate = createValidate;
945
954
 
946
955
  // src/api/set-errors.ts
947
956
  var createSetErrors = (state2, helpers) => (fields) => {
957
+ if (state2.isDestroyed) return state2.api;
948
958
  if (Array.isArray(fields)) {
949
959
  state2.manualErrors = {
950
960
  ...state2.manualErrors,
@@ -963,6 +973,7 @@ var createSetErrors = (state2, helpers) => (fields) => {
963
973
 
964
974
  // src/api/set-value.ts
965
975
  var createSetValue = (state2, helpers) => (name, value) => {
976
+ if (state2.isDestroyed) return state2.api;
966
977
  const fields = helpers.getFieldsByName(name);
967
978
  if (fields.length === 0) return state2.api;
968
979
  writeFieldValue(fields, value);
@@ -974,6 +985,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
974
985
 
975
986
  // src/api/subscribe.ts
976
987
  var createSubscribe = (state2, _helpers) => (listener) => {
988
+ if (state2.isDestroyed) return () => void 0;
977
989
  state2.subscribers.add(listener);
978
990
  listener(state2.api.getState());
979
991
  return () => {
@@ -1043,6 +1055,9 @@ var createFormController = (target, options = {}) => {
1043
1055
  submitCount: 0,
1044
1056
  pendingSubmitCount: 0,
1045
1057
  autoSubmitTimer: null,
1058
+ isAutoSubmitting: false,
1059
+ autoSubmitSuspended: false,
1060
+ isResetting: false,
1046
1061
  isDestroyed: false,
1047
1062
  listeners: [],
1048
1063
  mutationObserver: null,
@@ -1087,12 +1102,20 @@ var createFormController = (target, options = {}) => {
1087
1102
  state2.fieldCache.set(name, fields);
1088
1103
  return fields;
1089
1104
  };
1090
- const getTrackedFieldNames = () => {
1091
- if (!state2.element) return Object.keys(state2.validators);
1092
- const names = new Set(Object.keys(state2.validators));
1105
+ const indexFieldsByName = () => {
1106
+ const index = /* @__PURE__ */ new Map();
1107
+ if (!state2.element) return index;
1093
1108
  for (const field of getNamedFields(state2.element)) {
1094
- names.add(field.name);
1109
+ const bucket = index.get(field.name);
1110
+ if (bucket) bucket.push(field);
1111
+ else index.set(field.name, [field]);
1095
1112
  }
1113
+ return index;
1114
+ };
1115
+ const getTrackedFieldNames = (index = indexFieldsByName()) => {
1116
+ if (!state2.element) return Object.keys(state2.validators);
1117
+ const names = new Set(Object.keys(state2.validators));
1118
+ for (const name of index.keys()) names.add(name);
1096
1119
  for (const [canonical, entry] of state2.formattedFields) {
1097
1120
  names.add(canonical);
1098
1121
  if (entry.displayName && entry.displayName !== canonical) {
@@ -1103,8 +1126,9 @@ var createFormController = (target, options = {}) => {
1103
1126
  };
1104
1127
  const getValues = () => {
1105
1128
  const values = {};
1106
- for (const name of getTrackedFieldNames()) {
1107
- values[name] = readFieldValue(getFieldsByName(name));
1129
+ const index = indexFieldsByName();
1130
+ for (const name of getTrackedFieldNames(index)) {
1131
+ values[name] = readFieldValue(index.get(name) ?? []);
1108
1132
  }
1109
1133
  return values;
1110
1134
  };
@@ -1197,29 +1221,54 @@ var createFormController = (target, options = {}) => {
1197
1221
  const snapshot = state2.api.getState();
1198
1222
  callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1199
1223
  };
1224
+ const runAutoSubmit = () => {
1225
+ state2.autoSubmitTimer = null;
1226
+ if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
1227
+ state2.isAutoSubmitting = true;
1228
+ try {
1229
+ submitForm(state2.element);
1230
+ } finally {
1231
+ state2.isAutoSubmitting = false;
1232
+ }
1233
+ };
1200
1234
  const scheduleAutoSubmit = () => {
1201
- if (!state2.element || !state2.autoSubmitEnabled) return;
1235
+ if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
1202
1236
  if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
1237
+ state2.autoSubmitTimer = null;
1203
1238
  if (state2.autoSubmitDebounce > 0) {
1204
- state2.autoSubmitTimer = setTimeout(
1205
- () => submitForm(state2.element),
1206
- state2.autoSubmitDebounce
1207
- );
1239
+ state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
1208
1240
  return;
1209
1241
  }
1210
- submitForm(state2.element);
1242
+ runAutoSubmit();
1243
+ };
1244
+ const emitAllFieldWatchers = () => {
1245
+ for (const name of Array.from(state2.watchedFields.keys())) emitFieldWatchers(name);
1246
+ };
1247
+ const applyReset = () => {
1248
+ if (!state2.element || state2.isDestroyed) return;
1249
+ clearFieldCache();
1250
+ resetFormattedFields(state2);
1251
+ state2.manualErrors = {};
1252
+ state2.validationErrors = {};
1253
+ state2.validationGroupErrors = {};
1254
+ state2.validationElementErrors.clear();
1255
+ clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
1256
+ syncVisualState();
1257
+ emitAllFieldWatchers();
1258
+ notifySubscribers();
1211
1259
  };
1212
1260
  const helpers = {
1213
1261
  notifySubscribers,
1214
1262
  clearFieldCache,
1215
1263
  getFieldsByName,
1216
- getTrackedFieldNames,
1264
+ getTrackedFieldNames: () => getTrackedFieldNames(),
1217
1265
  getValues,
1218
1266
  getMergedErrors,
1219
1267
  syncVisualState,
1220
1268
  validateNames,
1221
1269
  emitFieldWatchers,
1222
- scheduleAutoSubmit
1270
+ scheduleAutoSubmit,
1271
+ applyReset
1223
1272
  };
1224
1273
  const handleDelegatedEvent = (event) => {
1225
1274
  const target2 = event.target;
@@ -1248,6 +1297,23 @@ var createFormController = (target, options = {}) => {
1248
1297
  if (target2.form !== state2.element || state2.element?.contains(target2)) return;
1249
1298
  handleDelegatedEvent(event);
1250
1299
  };
1300
+ const isFocusable = (candidate) => {
1301
+ if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
1302
+ return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
1303
+ };
1304
+ const focusFirstInvalidField = (errors) => {
1305
+ const candidates = [];
1306
+ for (const name of Object.keys(errors)) {
1307
+ const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
1308
+ const elementErrors = state2.validationElementErrors.get(name);
1309
+ const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
1310
+ if (field) candidates.push(field);
1311
+ }
1312
+ candidates.sort(
1313
+ (left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
1314
+ );
1315
+ candidates[0]?.focus();
1316
+ };
1251
1317
  const handleSubmitEvent = (event) => {
1252
1318
  if (!state2.element || state2.isDestroyed || !state2.api) return;
1253
1319
  if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
@@ -1258,23 +1324,7 @@ var createFormController = (target, options = {}) => {
1258
1324
  state2.submitCount += 1;
1259
1325
  notifySubscribers();
1260
1326
  if (!validation.isValid) {
1261
- for (const name of Object.keys(validation.errors)) {
1262
- const displayName = resolveDisplayNameForName(state2, name);
1263
- const fields = getFieldsByName(displayName);
1264
- const elementErrors = state2.validationElementErrors.get(name);
1265
- const field = fields.find((candidate2) => {
1266
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1267
- if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
1268
- return elementErrors?.has(candidate2);
1269
- }) ?? fields.find((candidate2) => {
1270
- if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1271
- return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
1272
- });
1273
- if (field) {
1274
- field.focus();
1275
- break;
1276
- }
1277
- }
1327
+ if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
1278
1328
  return;
1279
1329
  }
1280
1330
  const candidate = event;
@@ -1287,7 +1337,13 @@ var createFormController = (target, options = {}) => {
1287
1337
  try {
1288
1338
  for (const handler of handlers) {
1289
1339
  const result = handler.callback(state2.element, data, formData, snapshot);
1290
- if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
1340
+ if (result && typeof result.then === "function") {
1341
+ pending.push(
1342
+ Promise.resolve(result).catch((error) => {
1343
+ console.error("[forms] submit handler failed", error);
1344
+ })
1345
+ );
1346
+ }
1291
1347
  }
1292
1348
  } catch (error) {
1293
1349
  synchronousError = error;
@@ -1303,9 +1359,21 @@ var createFormController = (target, options = {}) => {
1303
1359
  }
1304
1360
  if (hasSynchronousError) throw synchronousError;
1305
1361
  };
1362
+ const handleResetEvent = (event) => {
1363
+ if (state2.isResetting) return;
1364
+ const timer = setTimeout(() => {
1365
+ state2.cleanups.delete(cancel);
1366
+ if (!event.defaultPrevented) applyReset();
1367
+ }, 0);
1368
+ const cancel = () => clearTimeout(timer);
1369
+ state2.cleanups.add(cancel);
1370
+ };
1306
1371
  const startMutationObserver = () => {
1307
1372
  if (!state2.element || typeof MutationObserver === "undefined") return;
1308
- state2.mutationObserver = new MutationObserver(() => {
1373
+ const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1374
+ const isRelevant = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1375
+ state2.mutationObserver = new MutationObserver((records) => {
1376
+ if (state2.isDestroyed || !records.some(isRelevant)) return;
1309
1377
  clearFieldCache();
1310
1378
  if (state2.isValidated) {
1311
1379
  validateNames();
@@ -1358,6 +1426,7 @@ var createFormController = (target, options = {}) => {
1358
1426
  addListener2(state2.element, "input", handleDelegatedEvent);
1359
1427
  addListener2(document, "input", handleExternalDelegatedEvent);
1360
1428
  addListener2(state2.element, "submit", handleSubmitEvent);
1429
+ addListener2(state2.element, "reset", handleResetEvent);
1361
1430
  startMutationObserver();
1362
1431
  }
1363
1432
  if (normalizedOptions.autoSubmit) {
@@ -1417,4 +1486,4 @@ export {
1417
1486
  form,
1418
1487
  browser
1419
1488
  };
1420
- //# sourceMappingURL=chunk-MMHX34WB.js.map
1489
+ //# sourceMappingURL=chunk-23VZAYA3.js.map