@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/index.cjs
CHANGED
|
@@ -59,6 +59,22 @@ 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;
|
|
@@ -486,12 +502,24 @@ var createDestroy = (state2, _helpers) => () => {
|
|
|
486
502
|
}
|
|
487
503
|
state2.mutationObserver?.disconnect();
|
|
488
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
|
+
}
|
|
489
514
|
state2.watchedFields.clear();
|
|
490
515
|
state2.subscribers.clear();
|
|
491
516
|
state2.submitHandlers.clear();
|
|
492
517
|
state2.fieldCache.clear();
|
|
493
518
|
state2.manualErrors = {};
|
|
494
519
|
state2.validationErrors = {};
|
|
520
|
+
state2.validationGroupErrors = {};
|
|
521
|
+
state2.validationElementErrors.clear();
|
|
522
|
+
state2.pendingSubmitCount = 0;
|
|
495
523
|
cleanupFormatRegistry(state2);
|
|
496
524
|
};
|
|
497
525
|
|
|
@@ -558,6 +586,7 @@ var createEmptyFormState = () => ({
|
|
|
558
586
|
isValid: true,
|
|
559
587
|
isValidated: false,
|
|
560
588
|
autoSubmit: false,
|
|
589
|
+
isSubmitting: false,
|
|
561
590
|
submitCount: 0
|
|
562
591
|
});
|
|
563
592
|
var cloneErrors = (errors) => Object.fromEntries(
|
|
@@ -589,6 +618,7 @@ var createGetState = (state2, helpers) => () => {
|
|
|
589
618
|
isValid: Object.keys(errors).length === 0,
|
|
590
619
|
isValidated: state2.isValidated,
|
|
591
620
|
autoSubmit: state2.autoSubmitEnabled,
|
|
621
|
+
isSubmitting: state2.pendingSubmitCount > 0,
|
|
592
622
|
submitCount: state2.submitCount
|
|
593
623
|
};
|
|
594
624
|
};
|
|
@@ -786,12 +816,14 @@ var createReset = (state2, helpers) => () => {
|
|
|
786
816
|
resetFormattedFields(state2);
|
|
787
817
|
state2.manualErrors = {};
|
|
788
818
|
state2.validationErrors = {};
|
|
819
|
+
state2.validationGroupErrors = {};
|
|
820
|
+
state2.validationElementErrors.clear();
|
|
789
821
|
clearAttributes(state2.element, [
|
|
790
822
|
state2.attributes.error,
|
|
791
823
|
state2.attributes.filled,
|
|
792
824
|
"aria-invalid"
|
|
793
825
|
]);
|
|
794
|
-
|
|
826
|
+
helpers.syncVisualState();
|
|
795
827
|
helpers.notifySubscribers();
|
|
796
828
|
return state2.api;
|
|
797
829
|
};
|
|
@@ -902,14 +934,29 @@ var valuesAreEqual = (left, right) => {
|
|
|
902
934
|
}
|
|
903
935
|
return left === right;
|
|
904
936
|
};
|
|
905
|
-
var
|
|
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 = {}) => {
|
|
906
946
|
const errors = [];
|
|
907
|
-
const context = {
|
|
947
|
+
const context = {
|
|
948
|
+
field,
|
|
949
|
+
value,
|
|
950
|
+
values,
|
|
951
|
+
...contextOverrides
|
|
952
|
+
};
|
|
908
953
|
const required = resolveRule(rules.required);
|
|
909
954
|
const minLength = resolveRule(rules.minLength);
|
|
910
955
|
const maxLength = resolveRule(rules.maxLength);
|
|
911
956
|
const pattern = resolveRule(rules.pattern);
|
|
912
|
-
const
|
|
957
|
+
const numeric = resolveRule(rules.numeric);
|
|
958
|
+
const min = resolveRule(rules.min);
|
|
959
|
+
const max = resolveRule(rules.max);
|
|
913
960
|
if (required.value && !hasValue(value)) {
|
|
914
961
|
errors.push(required.message ?? "This field is required.");
|
|
915
962
|
}
|
|
@@ -927,10 +974,17 @@ var validateFieldValue = (field, value, rules, values) => {
|
|
|
927
974
|
errors.push(pattern.message ?? "Value does not match the required pattern.");
|
|
928
975
|
}
|
|
929
976
|
}
|
|
930
|
-
if (
|
|
931
|
-
const
|
|
932
|
-
if (
|
|
933
|
-
errors.push(
|
|
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
|
+
}
|
|
934
988
|
}
|
|
935
989
|
}
|
|
936
990
|
const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
|
|
@@ -941,6 +995,41 @@ var validateFieldValue = (field, value, rules, values) => {
|
|
|
941
995
|
}
|
|
942
996
|
return errors;
|
|
943
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
|
+
};
|
|
944
1033
|
var validateValues = (values, schema) => {
|
|
945
1034
|
const errors = {};
|
|
946
1035
|
for (const [field, rules] of Object.entries(schema)) {
|
|
@@ -982,25 +1071,35 @@ var createFormController = (target, options = {}) => {
|
|
|
982
1071
|
fieldCache: /* @__PURE__ */ new Map(),
|
|
983
1072
|
manualErrors: {},
|
|
984
1073
|
validationErrors: {},
|
|
1074
|
+
validationGroupErrors: {},
|
|
1075
|
+
validationElementErrors: /* @__PURE__ */ new Map(),
|
|
985
1076
|
isValidated: Boolean(normalizedOptions.autoValidate),
|
|
986
1077
|
autoSubmitEnabled: false,
|
|
987
1078
|
autoSubmitDebounce: 0,
|
|
988
1079
|
submitCount: 0,
|
|
1080
|
+
pendingSubmitCount: 0,
|
|
989
1081
|
autoSubmitTimer: null,
|
|
990
1082
|
isDestroyed: false,
|
|
991
1083
|
listeners: [],
|
|
992
1084
|
mutationObserver: null,
|
|
1085
|
+
cleanups: /* @__PURE__ */ new Set(),
|
|
993
1086
|
formattedFields: /* @__PURE__ */ new Map(),
|
|
994
1087
|
api: null
|
|
995
1088
|
};
|
|
996
1089
|
const validationDependents = /* @__PURE__ */ new Map();
|
|
997
1090
|
for (const [field, rules] of Object.entries(state2.validators)) {
|
|
1091
|
+
const sources = /* @__PURE__ */ new Set();
|
|
998
1092
|
const sameAs = rules.sameAs;
|
|
999
|
-
if (sameAs
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
+
}
|
|
1004
1103
|
}
|
|
1005
1104
|
const notifySubscribers = () => {
|
|
1006
1105
|
if (!state2.api) return;
|
|
@@ -1049,18 +1148,21 @@ var createFormController = (target, options = {}) => {
|
|
|
1049
1148
|
const syncVisualState = (names) => {
|
|
1050
1149
|
if (!state2.element) return;
|
|
1051
1150
|
const sourceNames = names ?? getTrackedFieldNames();
|
|
1052
|
-
const errors = getMergedErrors();
|
|
1053
1151
|
for (const name of sourceNames) {
|
|
1054
1152
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
1055
1153
|
const canonical = resolveCanonicalForName(state2, name) ?? name;
|
|
1056
1154
|
const fields = getFieldsByName(displayName);
|
|
1057
|
-
const
|
|
1058
|
-
|
|
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);
|
|
1059
1161
|
applyBooleanAttribute(field, attributes.filled, isFieldFilled(field));
|
|
1060
1162
|
applyBooleanAttribute(field, attributes.error, hasError);
|
|
1061
1163
|
if (hasError) field.setAttribute("aria-invalid", "true");
|
|
1062
1164
|
else field.removeAttribute("aria-invalid");
|
|
1063
|
-
}
|
|
1165
|
+
});
|
|
1064
1166
|
}
|
|
1065
1167
|
};
|
|
1066
1168
|
void readFieldValue;
|
|
@@ -1069,17 +1171,38 @@ var createFormController = (target, options = {}) => {
|
|
|
1069
1171
|
const targetNames = names ?? Object.keys(state2.validators);
|
|
1070
1172
|
const values = getValues();
|
|
1071
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();
|
|
1072
1176
|
for (const name of targetNames) {
|
|
1073
1177
|
const rules = state2.validators[name];
|
|
1074
1178
|
if (!rules) {
|
|
1075
1179
|
delete nextValidationErrors[name];
|
|
1180
|
+
delete nextGroupErrors[name];
|
|
1181
|
+
nextElementErrors.delete(name);
|
|
1076
1182
|
continue;
|
|
1077
1183
|
}
|
|
1078
|
-
const
|
|
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)];
|
|
1079
1192
|
if (messages.length > 0) nextValidationErrors[name] = messages;
|
|
1080
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);
|
|
1081
1202
|
}
|
|
1082
1203
|
state2.validationErrors = nextValidationErrors;
|
|
1204
|
+
state2.validationGroupErrors = nextGroupErrors;
|
|
1205
|
+
state2.validationElementErrors = nextElementErrors;
|
|
1083
1206
|
syncVisualState(names);
|
|
1084
1207
|
return {
|
|
1085
1208
|
isValid: Object.keys(getMergedErrors()).length === 0,
|
|
@@ -1173,7 +1296,13 @@ var createFormController = (target, options = {}) => {
|
|
|
1173
1296
|
if (!validation.isValid) {
|
|
1174
1297
|
for (const name of Object.keys(validation.errors)) {
|
|
1175
1298
|
const displayName = resolveDisplayNameForName(state2, name);
|
|
1176
|
-
const
|
|
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) => {
|
|
1177
1306
|
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1178
1307
|
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
1179
1308
|
});
|
|
@@ -1188,18 +1317,35 @@ var createFormController = (target, options = {}) => {
|
|
|
1188
1317
|
const submitter = candidate.submitter instanceof HTMLElement ? candidate.submitter : null;
|
|
1189
1318
|
const { data, formData } = parseFormData(state2.element, submitter);
|
|
1190
1319
|
const snapshot = state2.api.getState();
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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;
|
|
1194
1341
|
};
|
|
1195
1342
|
const startMutationObserver = () => {
|
|
1196
1343
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
1197
1344
|
state2.mutationObserver = new MutationObserver(() => {
|
|
1198
1345
|
clearFieldCache();
|
|
1199
1346
|
if (state2.isValidated) {
|
|
1200
|
-
syncVisualState();
|
|
1201
1347
|
validateNames();
|
|
1202
|
-
}
|
|
1348
|
+
} else syncVisualState();
|
|
1203
1349
|
notifySubscribers();
|
|
1204
1350
|
});
|
|
1205
1351
|
state2.mutationObserver.observe(state2.element, {
|
|
@@ -1220,6 +1366,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1220
1366
|
return state2.options;
|
|
1221
1367
|
},
|
|
1222
1368
|
onSubmit: createOnSubmit(state2, helpers),
|
|
1369
|
+
addCleanup: createAddCleanup(state2, helpers),
|
|
1223
1370
|
watch: createWatch(state2, helpers),
|
|
1224
1371
|
observe: createObserve(state2, helpers),
|
|
1225
1372
|
unwatch: createUnwatch(state2, helpers),
|
|
@@ -1257,10 +1404,8 @@ var createFormController = (target, options = {}) => {
|
|
|
1257
1404
|
api.format(config);
|
|
1258
1405
|
}
|
|
1259
1406
|
}
|
|
1260
|
-
if (state2.isValidated)
|
|
1261
|
-
|
|
1262
|
-
validateNames();
|
|
1263
|
-
}
|
|
1407
|
+
if (state2.isValidated) validateNames();
|
|
1408
|
+
else syncVisualState();
|
|
1264
1409
|
return api;
|
|
1265
1410
|
};
|
|
1266
1411
|
|