@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.
- package/README.md +6 -4
- package/dist/browser/global.d.ts +19 -3
- package/dist/browser/global.global.js +169 -28
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +169 -28
- package/dist/browser/index.cjs.map +1 -1
- package/dist/browser/index.d.cts +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/{chunk-L4GRKSQ5.js → chunk-WGPAVJTK.js} +174 -29
- package/dist/chunk-WGPAVJTK.js.map +1 -0
- package/dist/index.cjs +173 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{registry-BpqJetAt.d.cts → registry-GzopYPCR.d.cts} +20 -4
- package/dist/{registry-BpqJetAt.d.ts → registry-GzopYPCR.d.ts} +20 -4
- package/docs/api/add-cleanup.md +52 -0
- package/docs/api/destroy.md +16 -1
- package/docs/api/form.md +4 -2
- package/docs/api/get-state.md +17 -1
- package/docs/api/index.md +1 -0
- package/docs/api/on-submit.md +7 -3
- package/docs/api/reset.md +3 -2
- package/docs/api/revalidate.md +3 -1
- package/docs/api/subscribe.md +16 -0
- package/docs/api/validate-field-value.md +39 -3
- package/docs/api/validate-values.md +4 -1
- package/docs/api/validate.md +5 -3
- package/docs/browser.md +5 -5
- package/docs/css-styling.md +5 -1
- package/docs/getting-started.md +13 -8
- package/docs/options.md +69 -2
- package/docs/recipes.md +64 -11
- package/docs/typescript.md +47 -8
- package/package.json +1 -1
- package/dist/chunk-L4GRKSQ5.js.map +0 -1
package/dist/browser/index.cjs
CHANGED
|
@@ -58,6 +58,22 @@ var createAppend = (state2, helpers) => ({ tag, content, class: className, atSta
|
|
|
58
58
|
return node;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
// src/api/add-cleanup.ts
|
|
62
|
+
var createAddCleanup = (state2, _helpers) => (cleanup) => {
|
|
63
|
+
if (state2.isDestroyed) {
|
|
64
|
+
cleanup();
|
|
65
|
+
return () => void 0;
|
|
66
|
+
}
|
|
67
|
+
const registration = () => cleanup();
|
|
68
|
+
state2.cleanups.add(registration);
|
|
69
|
+
let active = true;
|
|
70
|
+
return () => {
|
|
71
|
+
if (!active) return;
|
|
72
|
+
active = false;
|
|
73
|
+
state2.cleanups.delete(registration);
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
61
77
|
// src/api/auto-submit.ts
|
|
62
78
|
var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
63
79
|
state2.autoSubmitEnabled = next !== false;
|
|
@@ -485,12 +501,24 @@ var createDestroy = (state2, _helpers) => () => {
|
|
|
485
501
|
}
|
|
486
502
|
state2.mutationObserver?.disconnect();
|
|
487
503
|
state2.mutationObserver = null;
|
|
504
|
+
const cleanups = Array.from(state2.cleanups).reverse();
|
|
505
|
+
state2.cleanups.clear();
|
|
506
|
+
for (const cleanup of cleanups) {
|
|
507
|
+
try {
|
|
508
|
+
cleanup();
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error("[forms] cleanup failed", error);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
488
513
|
state2.watchedFields.clear();
|
|
489
514
|
state2.subscribers.clear();
|
|
490
515
|
state2.submitHandlers.clear();
|
|
491
516
|
state2.fieldCache.clear();
|
|
492
517
|
state2.manualErrors = {};
|
|
493
518
|
state2.validationErrors = {};
|
|
519
|
+
state2.validationGroupErrors = {};
|
|
520
|
+
state2.validationElementErrors.clear();
|
|
521
|
+
state2.pendingSubmitCount = 0;
|
|
494
522
|
cleanupFormatRegistry(state2);
|
|
495
523
|
};
|
|
496
524
|
|
|
@@ -557,6 +585,7 @@ var createEmptyFormState = () => ({
|
|
|
557
585
|
isValid: true,
|
|
558
586
|
isValidated: false,
|
|
559
587
|
autoSubmit: false,
|
|
588
|
+
isSubmitting: false,
|
|
560
589
|
submitCount: 0
|
|
561
590
|
});
|
|
562
591
|
var cloneErrors = (errors) => Object.fromEntries(
|
|
@@ -588,6 +617,7 @@ var createGetState = (state2, helpers) => () => {
|
|
|
588
617
|
isValid: Object.keys(errors).length === 0,
|
|
589
618
|
isValidated: state2.isValidated,
|
|
590
619
|
autoSubmit: state2.autoSubmitEnabled,
|
|
620
|
+
isSubmitting: state2.pendingSubmitCount > 0,
|
|
591
621
|
submitCount: state2.submitCount
|
|
592
622
|
};
|
|
593
623
|
};
|
|
@@ -785,12 +815,14 @@ var createReset = (state2, helpers) => () => {
|
|
|
785
815
|
resetFormattedFields(state2);
|
|
786
816
|
state2.manualErrors = {};
|
|
787
817
|
state2.validationErrors = {};
|
|
818
|
+
state2.validationGroupErrors = {};
|
|
819
|
+
state2.validationElementErrors.clear();
|
|
788
820
|
clearAttributes(state2.element, [
|
|
789
821
|
state2.attributes.error,
|
|
790
822
|
state2.attributes.filled,
|
|
791
823
|
"aria-invalid"
|
|
792
824
|
]);
|
|
793
|
-
|
|
825
|
+
helpers.syncVisualState();
|
|
794
826
|
helpers.notifySubscribers();
|
|
795
827
|
return state2.api;
|
|
796
828
|
};
|
|
@@ -901,14 +933,29 @@ var valuesAreEqual = (left, right) => {
|
|
|
901
933
|
}
|
|
902
934
|
return left === right;
|
|
903
935
|
};
|
|
904
|
-
var
|
|
936
|
+
var DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
937
|
+
var parseStrictNumber = (value) => {
|
|
938
|
+
if (typeof value !== "string") return void 0;
|
|
939
|
+
const candidate = value.trim();
|
|
940
|
+
if (!DECIMAL_PATTERN.test(candidate)) return void 0;
|
|
941
|
+
const number = Number(candidate);
|
|
942
|
+
return Number.isFinite(number) ? number : void 0;
|
|
943
|
+
};
|
|
944
|
+
var validateValue = (field, value, rules, values, contextOverrides = {}) => {
|
|
905
945
|
const errors = [];
|
|
906
|
-
const context = {
|
|
946
|
+
const context = {
|
|
947
|
+
field,
|
|
948
|
+
value,
|
|
949
|
+
values,
|
|
950
|
+
...contextOverrides
|
|
951
|
+
};
|
|
907
952
|
const required = resolveRule(rules.required);
|
|
908
953
|
const minLength = resolveRule(rules.minLength);
|
|
909
954
|
const maxLength = resolveRule(rules.maxLength);
|
|
910
955
|
const pattern = resolveRule(rules.pattern);
|
|
911
|
-
const
|
|
956
|
+
const numeric = resolveRule(rules.numeric);
|
|
957
|
+
const min = resolveRule(rules.min);
|
|
958
|
+
const max = resolveRule(rules.max);
|
|
912
959
|
if (required.value && !hasValue(value)) {
|
|
913
960
|
errors.push(required.message ?? "This field is required.");
|
|
914
961
|
}
|
|
@@ -926,10 +973,17 @@ var validateFieldValue = (field, value, rules, values) => {
|
|
|
926
973
|
errors.push(pattern.message ?? "Value does not match the required pattern.");
|
|
927
974
|
}
|
|
928
975
|
}
|
|
929
|
-
if (
|
|
930
|
-
const
|
|
931
|
-
if (
|
|
932
|
-
errors.push(
|
|
976
|
+
if (hasValue(value) && (numeric.value || min.value !== void 0 || max.value !== void 0)) {
|
|
977
|
+
const number = parseStrictNumber(value);
|
|
978
|
+
if (number === void 0) {
|
|
979
|
+
errors.push(numeric.message ?? "Value must be a number.");
|
|
980
|
+
} else {
|
|
981
|
+
if (min.value !== void 0 && number < min.value) {
|
|
982
|
+
errors.push(min.message ?? `Minimum value is ${min.value}.`);
|
|
983
|
+
}
|
|
984
|
+
if (max.value !== void 0 && number > max.value) {
|
|
985
|
+
errors.push(max.message ?? `Maximum value is ${max.value}.`);
|
|
986
|
+
}
|
|
933
987
|
}
|
|
934
988
|
}
|
|
935
989
|
const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
|
|
@@ -940,6 +994,37 @@ var validateFieldValue = (field, value, rules, values) => {
|
|
|
940
994
|
}
|
|
941
995
|
return errors;
|
|
942
996
|
};
|
|
997
|
+
var validateFieldValueDetailed = (field, value, rules, values, items) => {
|
|
998
|
+
const groupErrors = validateValue(field, value, rules, values);
|
|
999
|
+
const sameAs = resolveRule(rules.sameAs);
|
|
1000
|
+
if (sameAs.value !== void 0) {
|
|
1001
|
+
const otherValue = values[sameAs.value];
|
|
1002
|
+
if (hasValue(value) && hasValue(otherValue) && !valuesAreEqual(value, otherValue)) {
|
|
1003
|
+
groupErrors.push(sameAs.message ?? `Value must match ${sameAs.value}.`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const itemErrors = [];
|
|
1007
|
+
if (rules.each) {
|
|
1008
|
+
const sourceItems = items ?? (Array.isArray(value) ? value.map((entry) => ({
|
|
1009
|
+
value: typeof entry === "string" ? entry : [entry]
|
|
1010
|
+
})) : [{ value }]);
|
|
1011
|
+
sourceItems.forEach((item, index) => {
|
|
1012
|
+
const overrides = {
|
|
1013
|
+
index
|
|
1014
|
+
};
|
|
1015
|
+
if (item.element) overrides.element = item.element;
|
|
1016
|
+
const messages = validateValue(field, item.value, rules.each, values, overrides);
|
|
1017
|
+
if (messages.length === 0) return;
|
|
1018
|
+
const error = {
|
|
1019
|
+
index,
|
|
1020
|
+
messages
|
|
1021
|
+
};
|
|
1022
|
+
if (item.element) error.element = item.element;
|
|
1023
|
+
itemErrors.push(error);
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return { groupErrors, itemErrors };
|
|
1027
|
+
};
|
|
943
1028
|
|
|
944
1029
|
// src/core/controller.ts
|
|
945
1030
|
var DEFAULT_ATTRIBUTES = {
|
|
@@ -970,25 +1055,35 @@ var createFormController = (target, options = {}) => {
|
|
|
970
1055
|
fieldCache: /* @__PURE__ */ new Map(),
|
|
971
1056
|
manualErrors: {},
|
|
972
1057
|
validationErrors: {},
|
|
1058
|
+
validationGroupErrors: {},
|
|
1059
|
+
validationElementErrors: /* @__PURE__ */ new Map(),
|
|
973
1060
|
isValidated: Boolean(normalizedOptions.autoValidate),
|
|
974
1061
|
autoSubmitEnabled: false,
|
|
975
1062
|
autoSubmitDebounce: 0,
|
|
976
1063
|
submitCount: 0,
|
|
1064
|
+
pendingSubmitCount: 0,
|
|
977
1065
|
autoSubmitTimer: null,
|
|
978
1066
|
isDestroyed: false,
|
|
979
1067
|
listeners: [],
|
|
980
1068
|
mutationObserver: null,
|
|
1069
|
+
cleanups: /* @__PURE__ */ new Set(),
|
|
981
1070
|
formattedFields: /* @__PURE__ */ new Map(),
|
|
982
1071
|
api: null
|
|
983
1072
|
};
|
|
984
1073
|
const validationDependents = /* @__PURE__ */ new Map();
|
|
985
1074
|
for (const [field, rules] of Object.entries(state2.validators)) {
|
|
1075
|
+
const sources = /* @__PURE__ */ new Set();
|
|
986
1076
|
const sameAs = rules.sameAs;
|
|
987
|
-
if (sameAs
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1077
|
+
if (sameAs !== void 0) {
|
|
1078
|
+
sources.add(typeof sameAs === "string" ? sameAs : sameAs.value);
|
|
1079
|
+
}
|
|
1080
|
+
if (typeof rules.dependsOn === "string") sources.add(rules.dependsOn);
|
|
1081
|
+
else rules.dependsOn?.forEach((source) => sources.add(source));
|
|
1082
|
+
for (const source of sources) {
|
|
1083
|
+
const dependents = validationDependents.get(source) ?? /* @__PURE__ */ new Set();
|
|
1084
|
+
dependents.add(field);
|
|
1085
|
+
validationDependents.set(source, dependents);
|
|
1086
|
+
}
|
|
992
1087
|
}
|
|
993
1088
|
const notifySubscribers = () => {
|
|
994
1089
|
if (!state2.api) return;
|
|
@@ -1037,18 +1132,21 @@ var createFormController = (target, options = {}) => {
|
|
|
1037
1132
|
const syncVisualState = (names) => {
|
|
1038
1133
|
if (!state2.element) return;
|
|
1039
1134
|
const sourceNames = names ?? getTrackedFieldNames();
|
|
1040
|
-
const errors = getMergedErrors();
|
|
1041
1135
|
for (const name of sourceNames) {
|
|
1042
1136
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
1043
1137
|
const canonical = resolveCanonicalForName(state2, name) ?? name;
|
|
1044
1138
|
const fields = getFieldsByName(displayName);
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1139
|
+
const hasGroupError = Boolean(
|
|
1140
|
+
state2.validationGroupErrors[canonical]?.length || state2.manualErrors[canonical]?.length
|
|
1141
|
+
);
|
|
1142
|
+
const elementErrors = state2.validationElementErrors.get(canonical);
|
|
1143
|
+
fields.forEach((field) => {
|
|
1144
|
+
const hasError = hasGroupError || Boolean(elementErrors?.get(field)?.length);
|
|
1047
1145
|
applyBooleanAttribute(field, attributes.filled, isFieldFilled(field));
|
|
1048
1146
|
applyBooleanAttribute(field, attributes.error, hasError);
|
|
1049
1147
|
if (hasError) field.setAttribute("aria-invalid", "true");
|
|
1050
1148
|
else field.removeAttribute("aria-invalid");
|
|
1051
|
-
}
|
|
1149
|
+
});
|
|
1052
1150
|
}
|
|
1053
1151
|
};
|
|
1054
1152
|
void readFieldValue;
|
|
@@ -1057,17 +1155,38 @@ var createFormController = (target, options = {}) => {
|
|
|
1057
1155
|
const targetNames = names ?? Object.keys(state2.validators);
|
|
1058
1156
|
const values = getValues();
|
|
1059
1157
|
const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
|
|
1158
|
+
const nextGroupErrors = names ? cloneErrors(state2.validationGroupErrors) : {};
|
|
1159
|
+
const nextElementErrors = names ? new Map(state2.validationElementErrors) : /* @__PURE__ */ new Map();
|
|
1060
1160
|
for (const name of targetNames) {
|
|
1061
1161
|
const rules = state2.validators[name];
|
|
1062
1162
|
if (!rules) {
|
|
1063
1163
|
delete nextValidationErrors[name];
|
|
1164
|
+
delete nextGroupErrors[name];
|
|
1165
|
+
nextElementErrors.delete(name);
|
|
1064
1166
|
continue;
|
|
1065
1167
|
}
|
|
1066
|
-
const
|
|
1168
|
+
const fields = getFieldsByName(name);
|
|
1169
|
+
const displayFields = getFieldsByName(resolveDisplayNameForName(state2, name));
|
|
1170
|
+
const items = fields.map((field, index) => ({
|
|
1171
|
+
value: readFieldValue([field]),
|
|
1172
|
+
element: displayFields[index] ?? field
|
|
1173
|
+
}));
|
|
1174
|
+
const result = validateFieldValueDetailed(name, values[name], rules, values, items);
|
|
1175
|
+
const messages = [...result.groupErrors, ...result.itemErrors.flatMap((item) => item.messages)];
|
|
1067
1176
|
if (messages.length > 0) nextValidationErrors[name] = messages;
|
|
1068
1177
|
else delete nextValidationErrors[name];
|
|
1178
|
+
if (result.groupErrors.length > 0) nextGroupErrors[name] = result.groupErrors;
|
|
1179
|
+
else delete nextGroupErrors[name];
|
|
1180
|
+
const perElement = /* @__PURE__ */ new Map();
|
|
1181
|
+
result.itemErrors.forEach((item) => {
|
|
1182
|
+
if (item.element) perElement.set(item.element, item.messages);
|
|
1183
|
+
});
|
|
1184
|
+
if (perElement.size > 0) nextElementErrors.set(name, perElement);
|
|
1185
|
+
else nextElementErrors.delete(name);
|
|
1069
1186
|
}
|
|
1070
1187
|
state2.validationErrors = nextValidationErrors;
|
|
1188
|
+
state2.validationGroupErrors = nextGroupErrors;
|
|
1189
|
+
state2.validationElementErrors = nextElementErrors;
|
|
1071
1190
|
syncVisualState(names);
|
|
1072
1191
|
return {
|
|
1073
1192
|
isValid: Object.keys(getMergedErrors()).length === 0,
|
|
@@ -1161,7 +1280,13 @@ var createFormController = (target, options = {}) => {
|
|
|
1161
1280
|
if (!validation.isValid) {
|
|
1162
1281
|
for (const name of Object.keys(validation.errors)) {
|
|
1163
1282
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
1164
|
-
const
|
|
1283
|
+
const fields = getFieldsByName(displayName);
|
|
1284
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
1285
|
+
const field = fields.find((candidate2) => {
|
|
1286
|
+
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1287
|
+
if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
|
|
1288
|
+
return elementErrors?.has(candidate2);
|
|
1289
|
+
}) ?? fields.find((candidate2) => {
|
|
1165
1290
|
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1166
1291
|
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
1167
1292
|
});
|
|
@@ -1176,18 +1301,35 @@ var createFormController = (target, options = {}) => {
|
|
|
1176
1301
|
const submitter = candidate.submitter instanceof HTMLElement ? candidate.submitter : null;
|
|
1177
1302
|
const { data, formData } = parseFormData(state2.element, submitter);
|
|
1178
1303
|
const snapshot = state2.api.getState();
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1304
|
+
const pending = [];
|
|
1305
|
+
let synchronousError;
|
|
1306
|
+
let hasSynchronousError = false;
|
|
1307
|
+
try {
|
|
1308
|
+
for (const handler of handlers) {
|
|
1309
|
+
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
1310
|
+
if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
|
|
1311
|
+
}
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
synchronousError = error;
|
|
1314
|
+
hasSynchronousError = true;
|
|
1315
|
+
}
|
|
1316
|
+
if (pending.length > 0) {
|
|
1317
|
+
state2.pendingSubmitCount += 1;
|
|
1318
|
+
notifySubscribers();
|
|
1319
|
+
void Promise.allSettled(pending).then(() => {
|
|
1320
|
+
state2.pendingSubmitCount = Math.max(0, state2.pendingSubmitCount - 1);
|
|
1321
|
+
if (!state2.isDestroyed) notifySubscribers();
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
if (hasSynchronousError) throw synchronousError;
|
|
1182
1325
|
};
|
|
1183
1326
|
const startMutationObserver = () => {
|
|
1184
1327
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
1185
1328
|
state2.mutationObserver = new MutationObserver(() => {
|
|
1186
1329
|
clearFieldCache();
|
|
1187
1330
|
if (state2.isValidated) {
|
|
1188
|
-
syncVisualState();
|
|
1189
1331
|
validateNames();
|
|
1190
|
-
}
|
|
1332
|
+
} else syncVisualState();
|
|
1191
1333
|
notifySubscribers();
|
|
1192
1334
|
});
|
|
1193
1335
|
state2.mutationObserver.observe(state2.element, {
|
|
@@ -1208,6 +1350,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1208
1350
|
return state2.options;
|
|
1209
1351
|
},
|
|
1210
1352
|
onSubmit: createOnSubmit(state2, helpers),
|
|
1353
|
+
addCleanup: createAddCleanup(state2, helpers),
|
|
1211
1354
|
watch: createWatch(state2, helpers),
|
|
1212
1355
|
observe: createObserve(state2, helpers),
|
|
1213
1356
|
unwatch: createUnwatch(state2, helpers),
|
|
@@ -1245,10 +1388,8 @@ var createFormController = (target, options = {}) => {
|
|
|
1245
1388
|
api.format(config);
|
|
1246
1389
|
}
|
|
1247
1390
|
}
|
|
1248
|
-
if (state2.isValidated)
|
|
1249
|
-
|
|
1250
|
-
validateNames();
|
|
1251
|
-
}
|
|
1391
|
+
if (state2.isValidated) validateNames();
|
|
1392
|
+
else syncVisualState();
|
|
1252
1393
|
return api;
|
|
1253
1394
|
};
|
|
1254
1395
|
|