@samline/forms 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { t as FormsApi, u as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-BpqJetAt.cjs';
1
+ import { u as FormsApi, v as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-GzopYPCR.cjs';
2
2
 
3
3
  declare const Forms: FormsApi;
4
4
  declare const form: (target: FormTarget, options?: FormControllerOptions) => FormController;
@@ -1,4 +1,4 @@
1
- import { t as FormsApi, u as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-BpqJetAt.js';
1
+ import { u as FormsApi, v as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-GzopYPCR.js';
2
2
 
3
3
  declare const Forms: FormsApi;
4
4
  declare const form: (target: FormTarget, options?: FormControllerOptions) => FormController;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  browser
3
- } from "../chunk-L4GRKSQ5.js";
3
+ } from "../chunk-WGPAVJTK.js";
4
4
 
5
5
  // src/browser/global.ts
6
6
  var Forms = browser;
@@ -62,14 +62,29 @@ var valuesAreEqual = (left, right) => {
62
62
  }
63
63
  return left === right;
64
64
  };
65
- var validateFieldValue = (field, value, rules, values) => {
65
+ var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
66
+ var parseStrictNumber = (value) => {
67
+ if (typeof value !== "string") return void 0;
68
+ const candidate = value.trim();
69
+ if (!DECIMAL_PATTERN.test(candidate)) return void 0;
70
+ const number = Number(candidate);
71
+ return Number.isFinite(number) ? number : void 0;
72
+ };
73
+ var validateValue = (field, value, rules, values, contextOverrides = {}) => {
66
74
  const errors = [];
67
- const context = { field, value, values };
75
+ const context = {
76
+ field,
77
+ value,
78
+ values,
79
+ ...contextOverrides
80
+ };
68
81
  const required = resolveRule(rules.required);
69
82
  const minLength = resolveRule(rules.minLength);
70
83
  const maxLength = resolveRule(rules.maxLength);
71
84
  const pattern = resolveRule(rules.pattern);
72
- const sameAs = resolveRule(rules.sameAs);
85
+ const numeric = resolveRule(rules.numeric);
86
+ const min = resolveRule(rules.min);
87
+ const max = resolveRule(rules.max);
73
88
  if (required.value && !hasValue(value)) {
74
89
  errors.push(required.message ?? "This field is required.");
75
90
  }
@@ -87,10 +102,17 @@ var validateFieldValue = (field, value, rules, values) => {
87
102
  errors.push(pattern.message ?? "Value does not match the required pattern.");
88
103
  }
89
104
  }
90
- if (sameAs.value !== void 0) {
91
- const otherValue = values[sameAs.value];
92
- if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
93
- errors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
105
+ if (hasValue(value) && (numeric.value || min.value !== void 0 || max.value !== void 0)) {
106
+ const number = parseStrictNumber(value);
107
+ if (number === void 0) {
108
+ errors.push(numeric.message ?? "Value must be a number.");
109
+ } else {
110
+ if (min.value !== void 0 && number < min.value) {
111
+ errors.push(min.message ?? `Minimum value is ${min.value}.`);
112
+ }
113
+ if (max.value !== void 0 && number > max.value) {
114
+ errors.push(max.message ?? `Maximum value is ${max.value}.`);
115
+ }
94
116
  }
95
117
  }
96
118
  const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
@@ -101,6 +123,41 @@ var validateFieldValue = (field, value, rules, values) => {
101
123
  }
102
124
  return errors;
103
125
  };
126
+ var validateFieldValueDetailed = (field, value, rules, values, items) => {
127
+ const groupErrors = validateValue(field, value, rules, values);
128
+ const sameAs = resolveRule(rules.sameAs);
129
+ if (sameAs.value !== void 0) {
130
+ const otherValue = values[sameAs.value];
131
+ if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
132
+ groupErrors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
133
+ }
134
+ }
135
+ const itemErrors = [];
136
+ if (rules.each) {
137
+ const sourceItems = items ?? (Array.isArray(value) ? value.map((entry) => ({
138
+ value: typeof entry === "string" ? entry : [entry]
139
+ })) : [{ value }]);
140
+ sourceItems.forEach((item, index) => {
141
+ const overrides = {
142
+ index
143
+ };
144
+ if (item.element) overrides.element = item.element;
145
+ const messages = validateValue(field, item.value, rules.each, values, overrides);
146
+ if (messages.length === 0) return;
147
+ const error = {
148
+ index,
149
+ messages
150
+ };
151
+ if (item.element) error.element = item.element;
152
+ itemErrors.push(error);
153
+ });
154
+ }
155
+ return { groupErrors, itemErrors };
156
+ };
157
+ var validateFieldValue = (field, value, rules, values) => {
158
+ const result = validateFieldValueDetailed(field, value, rules, values);
159
+ return [...result.groupErrors, ...result.itemErrors.flatMap((item) => item.messages)];
160
+ };
104
161
  var validateValues = (values, schema) => {
105
162
  const errors = {};
106
163
  for (const [field, rules] of Object.entries(schema)) {
@@ -133,6 +190,22 @@ var createAppend = (state2, helpers) => ({ tag, content, class: className, atSta
133
190
  return node;
134
191
  };
135
192
 
193
+ // src/api/add-cleanup.ts
194
+ var createAddCleanup = (state2, _helpers) => (cleanup) => {
195
+ if (state2.isDestroyed) {
196
+ cleanup();
197
+ return () => void 0;
198
+ }
199
+ const registration = () => cleanup();
200
+ state2.cleanups.add(registration);
201
+ let active = true;
202
+ return () => {
203
+ if (!active) return;
204
+ active = false;
205
+ state2.cleanups.delete(registration);
206
+ };
207
+ };
208
+
136
209
  // src/api/auto-submit.ts
137
210
  var createAutoSubmit = (state2, helpers) => (next = true) => {
138
211
  state2.autoSubmitEnabled = next !== false;
@@ -560,12 +633,24 @@ var createDestroy = (state2, _helpers) => () => {
560
633
  }
561
634
  state2.mutationObserver?.disconnect();
562
635
  state2.mutationObserver = null;
636
+ const cleanups = Array.from(state2.cleanups).reverse();
637
+ state2.cleanups.clear();
638
+ for (const cleanup of cleanups) {
639
+ try {
640
+ cleanup();
641
+ } catch (error) {
642
+ console.error("[forms] cleanup failed", error);
643
+ }
644
+ }
563
645
  state2.watchedFields.clear();
564
646
  state2.subscribers.clear();
565
647
  state2.submitHandlers.clear();
566
648
  state2.fieldCache.clear();
567
649
  state2.manualErrors = {};
568
650
  state2.validationErrors = {};
651
+ state2.validationGroupErrors = {};
652
+ state2.validationElementErrors.clear();
653
+ state2.pendingSubmitCount = 0;
569
654
  cleanupFormatRegistry(state2);
570
655
  };
571
656
 
@@ -601,6 +686,7 @@ var createEmptyFormState = () => ({
601
686
  isValid: true,
602
687
  isValidated: false,
603
688
  autoSubmit: false,
689
+ isSubmitting: false,
604
690
  submitCount: 0
605
691
  });
606
692
  var cloneErrors = (errors) => Object.fromEntries(
@@ -632,6 +718,7 @@ var createGetState = (state2, helpers) => () => {
632
718
  isValid: Object.keys(errors).length === 0,
633
719
  isValidated: state2.isValidated,
634
720
  autoSubmit: state2.autoSubmitEnabled,
721
+ isSubmitting: state2.pendingSubmitCount > 0,
635
722
  submitCount: state2.submitCount
636
723
  };
637
724
  };
@@ -829,12 +916,14 @@ var createReset = (state2, helpers) => () => {
829
916
  resetFormattedFields(state2);
830
917
  state2.manualErrors = {};
831
918
  state2.validationErrors = {};
919
+ state2.validationGroupErrors = {};
920
+ state2.validationElementErrors.clear();
832
921
  clearAttributes(state2.element, [
833
922
  state2.attributes.error,
834
923
  state2.attributes.filled,
835
924
  "aria-invalid"
836
925
  ]);
837
- if (state2.isValidated) helpers.syncVisualState();
926
+ helpers.syncVisualState();
838
927
  helpers.notifySubscribers();
839
928
  return state2.api;
840
929
  };
@@ -941,25 +1030,35 @@ var createFormController = (target, options = {}) => {
941
1030
  fieldCache: /* @__PURE__ */ new Map(),
942
1031
  manualErrors: {},
943
1032
  validationErrors: {},
1033
+ validationGroupErrors: {},
1034
+ validationElementErrors: /* @__PURE__ */ new Map(),
944
1035
  isValidated: Boolean(normalizedOptions.autoValidate),
945
1036
  autoSubmitEnabled: false,
946
1037
  autoSubmitDebounce: 0,
947
1038
  submitCount: 0,
1039
+ pendingSubmitCount: 0,
948
1040
  autoSubmitTimer: null,
949
1041
  isDestroyed: false,
950
1042
  listeners: [],
951
1043
  mutationObserver: null,
1044
+ cleanups: /* @__PURE__ */ new Set(),
952
1045
  formattedFields: /* @__PURE__ */ new Map(),
953
1046
  api: null
954
1047
  };
955
1048
  const validationDependents = /* @__PURE__ */ new Map();
956
1049
  for (const [field, rules] of Object.entries(state2.validators)) {
1050
+ const sources = /* @__PURE__ */ new Set();
957
1051
  const sameAs = rules.sameAs;
958
- if (sameAs === void 0) continue;
959
- const source = typeof sameAs === "string" ? sameAs : sameAs.value;
960
- const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
961
- dependents.add(field);
962
- validationDependents.set(source, dependents);
1052
+ if (sameAs !== void 0) {
1053
+ sources.add(typeof sameAs === "string" ? sameAs : sameAs.value);
1054
+ }
1055
+ if (typeof rules.dependsOn === "string") sources.add(rules.dependsOn);
1056
+ else rules.dependsOn?.forEach((source) => sources.add(source));
1057
+ for (const source of sources) {
1058
+ const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
1059
+ dependents.add(field);
1060
+ validationDependents.set(source, dependents);
1061
+ }
963
1062
  }
964
1063
  const notifySubscribers = () => {
965
1064
  if (!state2.api) return;
@@ -1008,18 +1107,21 @@ var createFormController = (target, options = {}) => {
1008
1107
  const syncVisualState = (names) => {
1009
1108
  if (!state2.element) return;
1010
1109
  const sourceNames = names ?? getTrackedFieldNames();
1011
- const errors = getMergedErrors();
1012
1110
  for (const name of sourceNames) {
1013
1111
  const displayName = resolveDisplayNameForName(state2, name);
1014
1112
  const canonical = resolveCanonicalForName(state2, name) ?? name;
1015
1113
  const fields = getFieldsByName(displayName);
1016
- const hasError = Boolean(errors[canonical]?.length);
1017
- for (const field of fields) {
1114
+ const hasGroupError = Boolean(
1115
+ state2.validationGroupErrors[canonical]?.length || state2.manualErrors[canonical]?.length
1116
+ );
1117
+ const elementErrors = state2.validationElementErrors.get(canonical);
1118
+ fields.forEach((field) => {
1119
+ const hasError = hasGroupError || Boolean(elementErrors?.get(field)?.length);
1018
1120
  applyBooleanAttribute(field, attributes.filled, isFieldFilled(field));
1019
1121
  applyBooleanAttribute(field, attributes.error, hasError);
1020
1122
  if (hasError) field.setAttribute("aria-invalid", "true");
1021
1123
  else field.removeAttribute("aria-invalid");
1022
- }
1124
+ });
1023
1125
  }
1024
1126
  };
1025
1127
  void readFieldValue;
@@ -1028,17 +1130,38 @@ var createFormController = (target, options = {}) => {
1028
1130
  const targetNames = names ?? Object.keys(state2.validators);
1029
1131
  const values = getValues();
1030
1132
  const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
1133
+ const nextGroupErrors = names ? cloneErrors(state2.validationGroupErrors) : {};
1134
+ const nextElementErrors = names ? new Map(state2.validationElementErrors) : /* @__PURE__ */ new Map();
1031
1135
  for (const name of targetNames) {
1032
1136
  const rules = state2.validators[name];
1033
1137
  if (!rules) {
1034
1138
  delete nextValidationErrors[name];
1139
+ delete nextGroupErrors[name];
1140
+ nextElementErrors.delete(name);
1035
1141
  continue;
1036
1142
  }
1037
- const messages = validateFieldValue(name, values[name], rules, values);
1143
+ const fields = getFieldsByName(name);
1144
+ const displayFields = getFieldsByName(resolveDisplayNameForName(state2, name));
1145
+ const items = fields.map((field, index) => ({
1146
+ value: readFieldValue([field]),
1147
+ element: displayFields[index] ?? field
1148
+ }));
1149
+ const result = validateFieldValueDetailed(name, values[name], rules, values, items);
1150
+ const messages = [...result.groupErrors, ...result.itemErrors.flatMap((item) => item.messages)];
1038
1151
  if (messages.length > 0) nextValidationErrors[name] = messages;
1039
1152
  else delete nextValidationErrors[name];
1153
+ if (result.groupErrors.length > 0) nextGroupErrors[name] = result.groupErrors;
1154
+ else delete nextGroupErrors[name];
1155
+ const perElement = /* @__PURE__ */ new Map();
1156
+ result.itemErrors.forEach((item) => {
1157
+ if (item.element) perElement.set(item.element, item.messages);
1158
+ });
1159
+ if (perElement.size > 0) nextElementErrors.set(name, perElement);
1160
+ else nextElementErrors.delete(name);
1040
1161
  }
1041
1162
  state2.validationErrors = nextValidationErrors;
1163
+ state2.validationGroupErrors = nextGroupErrors;
1164
+ state2.validationElementErrors = nextElementErrors;
1042
1165
  syncVisualState(names);
1043
1166
  return {
1044
1167
  isValid: Object.keys(getMergedErrors()).length === 0,
@@ -1132,7 +1255,13 @@ var createFormController = (target, options = {}) => {
1132
1255
  if (!validation.isValid) {
1133
1256
  for (const name of Object.keys(validation.errors)) {
1134
1257
  const displayName = resolveDisplayNameForName(state2, name);
1135
- const field = getFieldsByName(displayName).find((candidate2) => {
1258
+ const fields = getFieldsByName(displayName);
1259
+ const elementErrors = state2.validationElementErrors.get(name);
1260
+ const field = fields.find((candidate2) => {
1261
+ if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1262
+ if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
1263
+ return elementErrors?.has(candidate2);
1264
+ }) ?? fields.find((candidate2) => {
1136
1265
  if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
1137
1266
  return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
1138
1267
  });
@@ -1147,18 +1276,35 @@ var createFormController = (target, options = {}) => {
1147
1276
  const submitter = candidate.submitter instanceof HTMLElement ? candidate.submitter : null;
1148
1277
  const { data, formData } = parseFormData(state2.element, submitter);
1149
1278
  const snapshot = state2.api.getState();
1150
- handlers.forEach(
1151
- (handler) => handler.callback(state2.element, data, formData, snapshot)
1152
- );
1279
+ const pending = [];
1280
+ let synchronousError;
1281
+ let hasSynchronousError = false;
1282
+ try {
1283
+ for (const handler of handlers) {
1284
+ const result = handler.callback(state2.element, data, formData, snapshot);
1285
+ if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
1286
+ }
1287
+ } catch (error) {
1288
+ synchronousError = error;
1289
+ hasSynchronousError = true;
1290
+ }
1291
+ if (pending.length > 0) {
1292
+ state2.pendingSubmitCount += 1;
1293
+ notifySubscribers();
1294
+ void Promise.allSettled(pending).then(() => {
1295
+ state2.pendingSubmitCount = Math.max(0, state2.pendingSubmitCount - 1);
1296
+ if (!state2.isDestroyed) notifySubscribers();
1297
+ });
1298
+ }
1299
+ if (hasSynchronousError) throw synchronousError;
1153
1300
  };
1154
1301
  const startMutationObserver = () => {
1155
1302
  if (!state2.element || typeof MutationObserver === "undefined") return;
1156
1303
  state2.mutationObserver = new MutationObserver(() => {
1157
1304
  clearFieldCache();
1158
1305
  if (state2.isValidated) {
1159
- syncVisualState();
1160
1306
  validateNames();
1161
- }
1307
+ } else syncVisualState();
1162
1308
  notifySubscribers();
1163
1309
  });
1164
1310
  state2.mutationObserver.observe(state2.element, {
@@ -1179,6 +1325,7 @@ var createFormController = (target, options = {}) => {
1179
1325
  return state2.options;
1180
1326
  },
1181
1327
  onSubmit: createOnSubmit(state2, helpers),
1328
+ addCleanup: createAddCleanup(state2, helpers),
1182
1329
  watch: createWatch(state2, helpers),
1183
1330
  observe: createObserve(state2, helpers),
1184
1331
  unwatch: createUnwatch(state2, helpers),
@@ -1216,10 +1363,8 @@ var createFormController = (target, options = {}) => {
1216
1363
  api.format(config);
1217
1364
  }
1218
1365
  }
1219
- if (state2.isValidated) {
1220
- syncVisualState();
1221
- validateNames();
1222
- }
1366
+ if (state2.isValidated) validateNames();
1367
+ else syncVisualState();
1223
1368
  return api;
1224
1369
  };
1225
1370
 
@@ -1267,4 +1412,4 @@ export {
1267
1412
  form,
1268
1413
  browser
1269
1414
  };
1270
- //# sourceMappingURL=chunk-L4GRKSQ5.js.map
1415
+ //# sourceMappingURL=chunk-WGPAVJTK.js.map