@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.
- package/CHANGELOG.md +302 -0
- package/README.md +110 -122
- package/dist/browser/global.d.ts +1 -13
- package/dist/browser/global.global.js +12 -6188
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +125 -56
- package/dist/browser/index.cjs.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/{chunk-MMHX34WB.js → chunk-23VZAYA3.js} +126 -57
- package/dist/chunk-23VZAYA3.js.map +1 -0
- package/dist/index.cjs +125 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +35 -7
- package/dist/chunk-MMHX34WB.js.map +0 -1
- package/docs/api/add-cleanup.md +0 -52
- package/docs/api/append.md +0 -96
- package/docs/api/auto-submit.md +0 -91
- package/docs/api/clear-errors.md +0 -69
- package/docs/api/destroy.md +0 -80
- package/docs/api/disable-auto-submit.md +0 -65
- package/docs/api/element.md +0 -78
- package/docs/api/form.md +0 -156
- package/docs/api/format.md +0 -270
- package/docs/api/get-data.md +0 -90
- package/docs/api/get-field.md +0 -87
- package/docs/api/get-state.md +0 -111
- package/docs/api/get-value.md +0 -114
- package/docs/api/index.md +0 -55
- package/docs/api/observe.md +0 -126
- package/docs/api/on-submit.md +0 -94
- package/docs/api/parse-form-data.md +0 -94
- package/docs/api/prefill.md +0 -89
- package/docs/api/regex.md +0 -155
- package/docs/api/reset.md +0 -61
- package/docs/api/revalidate.md +0 -52
- package/docs/api/set-errors.md +0 -97
- package/docs/api/set-value.md +0 -120
- package/docs/api/subscribe.md +0 -107
- package/docs/api/unwatch.md +0 -74
- package/docs/api/validate-field-value.md +0 -144
- package/docs/api/validate-values.md +0 -103
- package/docs/api/validate.md +0 -102
- package/docs/api/watch.md +0 -117
- package/docs/browser.md +0 -156
- package/docs/css-styling.md +0 -191
- package/docs/getting-started.md +0 -222
- package/docs/options.md +0 -326
- package/docs/recipes.md +0 -531
- package/docs/typescript.md +0 -615
- package/docs/vanilla.md +0 -12
- /package/{LICENSE → LICENSE.md} +0 -0
package/dist/browser/index.cjs
CHANGED
|
@@ -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 = () => {
|
|
@@ -608,7 +611,7 @@ var mergeErrors = (left, right) => {
|
|
|
608
611
|
var createGetState = (state2, helpers) => () => {
|
|
609
612
|
const values = helpers.getValues();
|
|
610
613
|
const errors = cloneErrors(helpers.getMergedErrors());
|
|
611
|
-
const filledFields =
|
|
614
|
+
const filledFields = Object.keys(values).filter((name) => {
|
|
612
615
|
const value = values[name];
|
|
613
616
|
if (Array.isArray(value)) return value.length > 0;
|
|
614
617
|
if (typeof value === "string") return value.trim().length > 0;
|
|
@@ -781,6 +784,7 @@ var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFi
|
|
|
781
784
|
|
|
782
785
|
// src/api/observe.ts
|
|
783
786
|
var createObserve = (state2, helpers) => (field, callback) => {
|
|
787
|
+
if (state2.isDestroyed) return () => void 0;
|
|
784
788
|
const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
|
|
785
789
|
callbacks.add(callback);
|
|
786
790
|
state2.watchedFields.set(field, callbacks);
|
|
@@ -798,44 +802,49 @@ var createObserve = (state2, helpers) => (field, callback) => {
|
|
|
798
802
|
|
|
799
803
|
// src/api/on-submit.ts
|
|
800
804
|
var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
|
|
805
|
+
if (state2.isDestroyed) return state2.api;
|
|
801
806
|
state2.submitHandlers.add({ callback, preventDefault });
|
|
802
807
|
return state2.api;
|
|
803
808
|
};
|
|
804
809
|
|
|
805
810
|
// src/api/prefill.ts
|
|
806
|
-
var createPrefill = (state2,
|
|
807
|
-
if (!state2.element) return state2.api;
|
|
811
|
+
var createPrefill = (state2, helpers) => (fieldName) => {
|
|
812
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
808
813
|
const params = new URLSearchParams(window.location.search);
|
|
809
|
-
params.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
814
|
+
const keys = Array.from(new Set(params.keys())).filter(
|
|
815
|
+
(key) => !fieldName || fieldName === key
|
|
816
|
+
);
|
|
817
|
+
if (keys.length === 0) return state2.api;
|
|
818
|
+
state2.autoSubmitSuspended = true;
|
|
819
|
+
try {
|
|
820
|
+
for (const key of keys) {
|
|
821
|
+
const values = params.getAll(key);
|
|
822
|
+
state2.api.setValue(key, values.length > 1 ? values : values[0]);
|
|
823
|
+
}
|
|
824
|
+
} finally {
|
|
825
|
+
state2.autoSubmitSuspended = false;
|
|
826
|
+
}
|
|
827
|
+
helpers.scheduleAutoSubmit();
|
|
813
828
|
return state2.api;
|
|
814
829
|
};
|
|
815
830
|
|
|
816
831
|
// src/api/reset.ts
|
|
817
832
|
var createReset = (state2, helpers) => () => {
|
|
818
|
-
if (!state2.element) return state2.api;
|
|
819
|
-
state2.
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
state2.attributes.error,
|
|
827
|
-
state2.attributes.filled,
|
|
828
|
-
"aria-invalid"
|
|
829
|
-
]);
|
|
830
|
-
helpers.syncVisualState();
|
|
831
|
-
helpers.notifySubscribers();
|
|
833
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
834
|
+
state2.isResetting = true;
|
|
835
|
+
try {
|
|
836
|
+
state2.element.reset();
|
|
837
|
+
} finally {
|
|
838
|
+
state2.isResetting = false;
|
|
839
|
+
}
|
|
840
|
+
helpers.applyReset();
|
|
832
841
|
return state2.api;
|
|
833
842
|
};
|
|
834
843
|
|
|
835
844
|
// src/api/validate.ts
|
|
836
845
|
var createValidate = (state2, helpers) => (fields) => {
|
|
846
|
+
if (state2.isDestroyed) return { isValid: true, errors: {} };
|
|
837
847
|
state2.isValidated = true;
|
|
838
|
-
helpers.syncVisualState(fields);
|
|
839
848
|
return helpers.validateNames(fields);
|
|
840
849
|
};
|
|
841
850
|
|
|
@@ -844,6 +853,7 @@ var createRevalidate = createValidate;
|
|
|
844
853
|
|
|
845
854
|
// src/api/set-errors.ts
|
|
846
855
|
var createSetErrors = (state2, helpers) => (fields) => {
|
|
856
|
+
if (state2.isDestroyed) return state2.api;
|
|
847
857
|
if (Array.isArray(fields)) {
|
|
848
858
|
state2.manualErrors = {
|
|
849
859
|
...state2.manualErrors,
|
|
@@ -862,6 +872,7 @@ var createSetErrors = (state2, helpers) => (fields) => {
|
|
|
862
872
|
|
|
863
873
|
// src/api/set-value.ts
|
|
864
874
|
var createSetValue = (state2, helpers) => (name, value) => {
|
|
875
|
+
if (state2.isDestroyed) return state2.api;
|
|
865
876
|
const fields = helpers.getFieldsByName(name);
|
|
866
877
|
if (fields.length === 0) return state2.api;
|
|
867
878
|
writeFieldValue(fields, value);
|
|
@@ -873,6 +884,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
|
|
|
873
884
|
|
|
874
885
|
// src/api/subscribe.ts
|
|
875
886
|
var createSubscribe = (state2, _helpers) => (listener) => {
|
|
887
|
+
if (state2.isDestroyed) return () => void 0;
|
|
876
888
|
state2.subscribers.add(listener);
|
|
877
889
|
listener(state2.api.getState());
|
|
878
890
|
return () => {
|
|
@@ -1068,6 +1080,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1068
1080
|
submitCount: 0,
|
|
1069
1081
|
pendingSubmitCount: 0,
|
|
1070
1082
|
autoSubmitTimer: null,
|
|
1083
|
+
isAutoSubmitting: false,
|
|
1084
|
+
autoSubmitSuspended: false,
|
|
1085
|
+
isResetting: false,
|
|
1071
1086
|
isDestroyed: false,
|
|
1072
1087
|
listeners: [],
|
|
1073
1088
|
mutationObserver: null,
|
|
@@ -1112,12 +1127,20 @@ var createFormController = (target, options = {}) => {
|
|
|
1112
1127
|
state2.fieldCache.set(name, fields);
|
|
1113
1128
|
return fields;
|
|
1114
1129
|
};
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1130
|
+
const indexFieldsByName = () => {
|
|
1131
|
+
const index = /* @__PURE__ */ new Map();
|
|
1132
|
+
if (!state2.element) return index;
|
|
1118
1133
|
for (const field of getNamedFields(state2.element)) {
|
|
1119
|
-
|
|
1134
|
+
const bucket = index.get(field.name);
|
|
1135
|
+
if (bucket) bucket.push(field);
|
|
1136
|
+
else index.set(field.name, [field]);
|
|
1120
1137
|
}
|
|
1138
|
+
return index;
|
|
1139
|
+
};
|
|
1140
|
+
const getTrackedFieldNames = (index = indexFieldsByName()) => {
|
|
1141
|
+
if (!state2.element) return Object.keys(state2.validators);
|
|
1142
|
+
const names = new Set(Object.keys(state2.validators));
|
|
1143
|
+
for (const name of index.keys()) names.add(name);
|
|
1121
1144
|
for (const [canonical, entry] of state2.formattedFields) {
|
|
1122
1145
|
names.add(canonical);
|
|
1123
1146
|
if (entry.displayName && entry.displayName !== canonical) {
|
|
@@ -1128,8 +1151,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1128
1151
|
};
|
|
1129
1152
|
const getValues = () => {
|
|
1130
1153
|
const values = {};
|
|
1131
|
-
|
|
1132
|
-
|
|
1154
|
+
const index = indexFieldsByName();
|
|
1155
|
+
for (const name of getTrackedFieldNames(index)) {
|
|
1156
|
+
values[name] = readFieldValue(index.get(name) ?? []);
|
|
1133
1157
|
}
|
|
1134
1158
|
return values;
|
|
1135
1159
|
};
|
|
@@ -1222,29 +1246,54 @@ var createFormController = (target, options = {}) => {
|
|
|
1222
1246
|
const snapshot = state2.api.getState();
|
|
1223
1247
|
callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
|
|
1224
1248
|
};
|
|
1249
|
+
const runAutoSubmit = () => {
|
|
1250
|
+
state2.autoSubmitTimer = null;
|
|
1251
|
+
if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
|
|
1252
|
+
state2.isAutoSubmitting = true;
|
|
1253
|
+
try {
|
|
1254
|
+
submitForm(state2.element);
|
|
1255
|
+
} finally {
|
|
1256
|
+
state2.isAutoSubmitting = false;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1225
1259
|
const scheduleAutoSubmit = () => {
|
|
1226
|
-
if (!state2.element || !state2.autoSubmitEnabled) return;
|
|
1260
|
+
if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
|
|
1227
1261
|
if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
|
|
1262
|
+
state2.autoSubmitTimer = null;
|
|
1228
1263
|
if (state2.autoSubmitDebounce > 0) {
|
|
1229
|
-
state2.autoSubmitTimer = setTimeout(
|
|
1230
|
-
() => submitForm(state2.element),
|
|
1231
|
-
state2.autoSubmitDebounce
|
|
1232
|
-
);
|
|
1264
|
+
state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
|
|
1233
1265
|
return;
|
|
1234
1266
|
}
|
|
1235
|
-
|
|
1267
|
+
runAutoSubmit();
|
|
1268
|
+
};
|
|
1269
|
+
const emitAllFieldWatchers = () => {
|
|
1270
|
+
for (const name of Array.from(state2.watchedFields.keys())) emitFieldWatchers(name);
|
|
1271
|
+
};
|
|
1272
|
+
const applyReset = () => {
|
|
1273
|
+
if (!state2.element || state2.isDestroyed) return;
|
|
1274
|
+
clearFieldCache();
|
|
1275
|
+
resetFormattedFields(state2);
|
|
1276
|
+
state2.manualErrors = {};
|
|
1277
|
+
state2.validationErrors = {};
|
|
1278
|
+
state2.validationGroupErrors = {};
|
|
1279
|
+
state2.validationElementErrors.clear();
|
|
1280
|
+
clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
|
|
1281
|
+
syncVisualState();
|
|
1282
|
+
emitAllFieldWatchers();
|
|
1283
|
+
notifySubscribers();
|
|
1236
1284
|
};
|
|
1237
1285
|
const helpers = {
|
|
1238
1286
|
notifySubscribers,
|
|
1239
1287
|
clearFieldCache,
|
|
1240
1288
|
getFieldsByName,
|
|
1241
|
-
getTrackedFieldNames,
|
|
1289
|
+
getTrackedFieldNames: () => getTrackedFieldNames(),
|
|
1242
1290
|
getValues,
|
|
1243
1291
|
getMergedErrors,
|
|
1244
1292
|
syncVisualState,
|
|
1245
1293
|
validateNames,
|
|
1246
1294
|
emitFieldWatchers,
|
|
1247
|
-
scheduleAutoSubmit
|
|
1295
|
+
scheduleAutoSubmit,
|
|
1296
|
+
applyReset
|
|
1248
1297
|
};
|
|
1249
1298
|
const handleDelegatedEvent = (event) => {
|
|
1250
1299
|
const target2 = event.target;
|
|
@@ -1273,6 +1322,23 @@ var createFormController = (target, options = {}) => {
|
|
|
1273
1322
|
if (target2.form !== state2.element || state2.element?.contains(target2)) return;
|
|
1274
1323
|
handleDelegatedEvent(event);
|
|
1275
1324
|
};
|
|
1325
|
+
const isFocusable = (candidate) => {
|
|
1326
|
+
if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
|
|
1327
|
+
return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
|
|
1328
|
+
};
|
|
1329
|
+
const focusFirstInvalidField = (errors) => {
|
|
1330
|
+
const candidates = [];
|
|
1331
|
+
for (const name of Object.keys(errors)) {
|
|
1332
|
+
const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
|
|
1333
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
1334
|
+
const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
|
|
1335
|
+
if (field) candidates.push(field);
|
|
1336
|
+
}
|
|
1337
|
+
candidates.sort(
|
|
1338
|
+
(left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
1339
|
+
);
|
|
1340
|
+
candidates[0]?.focus();
|
|
1341
|
+
};
|
|
1276
1342
|
const handleSubmitEvent = (event) => {
|
|
1277
1343
|
if (!state2.element || state2.isDestroyed || !state2.api) return;
|
|
1278
1344
|
if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
|
|
@@ -1283,23 +1349,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1283
1349
|
state2.submitCount += 1;
|
|
1284
1350
|
notifySubscribers();
|
|
1285
1351
|
if (!validation.isValid) {
|
|
1286
|
-
|
|
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
|
-
}
|
|
1352
|
+
if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
|
|
1303
1353
|
return;
|
|
1304
1354
|
}
|
|
1305
1355
|
const candidate = event;
|
|
@@ -1312,7 +1362,13 @@ var createFormController = (target, options = {}) => {
|
|
|
1312
1362
|
try {
|
|
1313
1363
|
for (const handler of handlers) {
|
|
1314
1364
|
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
1315
|
-
if (result && typeof result.then === "function")
|
|
1365
|
+
if (result && typeof result.then === "function") {
|
|
1366
|
+
pending.push(
|
|
1367
|
+
Promise.resolve(result).catch((error) => {
|
|
1368
|
+
console.error("[forms] submit handler failed", error);
|
|
1369
|
+
})
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1316
1372
|
}
|
|
1317
1373
|
} catch (error) {
|
|
1318
1374
|
synchronousError = error;
|
|
@@ -1328,9 +1384,21 @@ var createFormController = (target, options = {}) => {
|
|
|
1328
1384
|
}
|
|
1329
1385
|
if (hasSynchronousError) throw synchronousError;
|
|
1330
1386
|
};
|
|
1387
|
+
const handleResetEvent = (event) => {
|
|
1388
|
+
if (state2.isResetting) return;
|
|
1389
|
+
const timer = setTimeout(() => {
|
|
1390
|
+
state2.cleanups.delete(cancel);
|
|
1391
|
+
if (!event.defaultPrevented) applyReset();
|
|
1392
|
+
}, 0);
|
|
1393
|
+
const cancel = () => clearTimeout(timer);
|
|
1394
|
+
state2.cleanups.add(cancel);
|
|
1395
|
+
};
|
|
1331
1396
|
const startMutationObserver = () => {
|
|
1332
1397
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
1333
|
-
|
|
1398
|
+
const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
|
|
1399
|
+
const isRelevant = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
|
|
1400
|
+
state2.mutationObserver = new MutationObserver((records) => {
|
|
1401
|
+
if (state2.isDestroyed || !records.some(isRelevant)) return;
|
|
1334
1402
|
clearFieldCache();
|
|
1335
1403
|
if (state2.isValidated) {
|
|
1336
1404
|
validateNames();
|
|
@@ -1383,6 +1451,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1383
1451
|
addListener2(state2.element, "input", handleDelegatedEvent);
|
|
1384
1452
|
addListener2(document, "input", handleExternalDelegatedEvent);
|
|
1385
1453
|
addListener2(state2.element, "submit", handleSubmitEvent);
|
|
1454
|
+
addListener2(state2.element, "reset", handleResetEvent);
|
|
1386
1455
|
startMutationObserver();
|
|
1387
1456
|
}
|
|
1388
1457
|
if (normalizedOptions.autoSubmit) {
|