@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/index.cjs
CHANGED
|
@@ -41,7 +41,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
41
41
|
|
|
42
42
|
// src/api/append.ts
|
|
43
43
|
var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
|
|
44
|
-
if (!state2.element) return null;
|
|
44
|
+
if (!state2.element || state2.isDestroyed) return null;
|
|
45
45
|
helpers.clearFieldCache();
|
|
46
46
|
if (className) {
|
|
47
47
|
const classSelector = className.trim().split(/\s+/)[0];
|
|
@@ -77,6 +77,7 @@ var createAddCleanup = (state2, _helpers) => (cleanup) => {
|
|
|
77
77
|
|
|
78
78
|
// src/api/auto-submit.ts
|
|
79
79
|
var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
80
|
+
if (state2.isDestroyed) return state2.api;
|
|
80
81
|
state2.autoSubmitEnabled = next !== false;
|
|
81
82
|
if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
|
|
82
83
|
clearTimeout(state2.autoSubmitTimer);
|
|
@@ -89,6 +90,7 @@ var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
|
89
90
|
|
|
90
91
|
// src/api/clear-errors.ts
|
|
91
92
|
var createClearErrors = (state2, helpers) => (fields) => {
|
|
93
|
+
if (state2.isDestroyed) return state2.api;
|
|
92
94
|
if (!fields) {
|
|
93
95
|
state2.manualErrors = {};
|
|
94
96
|
helpers.syncVisualState();
|
|
@@ -215,6 +217,7 @@ var isFormatChar = (char) => {
|
|
|
215
217
|
return true;
|
|
216
218
|
};
|
|
217
219
|
var restoreCursor = (field, position) => {
|
|
220
|
+
if (field.ownerDocument.activeElement !== field) return;
|
|
218
221
|
const max = field.value.length;
|
|
219
222
|
const safe = clamp(position, 0, max);
|
|
220
223
|
const apply = () => {
|
|
@@ -609,7 +612,7 @@ var mergeErrors = (left, right) => {
|
|
|
609
612
|
var createGetState = (state2, helpers) => () => {
|
|
610
613
|
const values = helpers.getValues();
|
|
611
614
|
const errors = cloneErrors(helpers.getMergedErrors());
|
|
612
|
-
const filledFields =
|
|
615
|
+
const filledFields = Object.keys(values).filter((name) => {
|
|
613
616
|
const value = values[name];
|
|
614
617
|
if (Array.isArray(value)) return value.length > 0;
|
|
615
618
|
if (typeof value === "string") return value.trim().length > 0;
|
|
@@ -782,6 +785,7 @@ var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFi
|
|
|
782
785
|
|
|
783
786
|
// src/api/observe.ts
|
|
784
787
|
var createObserve = (state2, helpers) => (field, callback) => {
|
|
788
|
+
if (state2.isDestroyed) return () => void 0;
|
|
785
789
|
const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
|
|
786
790
|
callbacks.add(callback);
|
|
787
791
|
state2.watchedFields.set(field, callbacks);
|
|
@@ -799,44 +803,49 @@ var createObserve = (state2, helpers) => (field, callback) => {
|
|
|
799
803
|
|
|
800
804
|
// src/api/on-submit.ts
|
|
801
805
|
var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
|
|
806
|
+
if (state2.isDestroyed) return state2.api;
|
|
802
807
|
state2.submitHandlers.add({ callback, preventDefault });
|
|
803
808
|
return state2.api;
|
|
804
809
|
};
|
|
805
810
|
|
|
806
811
|
// src/api/prefill.ts
|
|
807
|
-
var createPrefill = (state2,
|
|
808
|
-
if (!state2.element) return state2.api;
|
|
812
|
+
var createPrefill = (state2, helpers) => (fieldName) => {
|
|
813
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
809
814
|
const params = new URLSearchParams(window.location.search);
|
|
810
|
-
params.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
815
|
+
const keys = Array.from(new Set(params.keys())).filter(
|
|
816
|
+
(key) => !fieldName || fieldName === key
|
|
817
|
+
);
|
|
818
|
+
if (keys.length === 0) return state2.api;
|
|
819
|
+
state2.autoSubmitSuspended = true;
|
|
820
|
+
try {
|
|
821
|
+
for (const key of keys) {
|
|
822
|
+
const values = params.getAll(key);
|
|
823
|
+
state2.api.setValue(key, values.length > 1 ? values : values[0]);
|
|
824
|
+
}
|
|
825
|
+
} finally {
|
|
826
|
+
state2.autoSubmitSuspended = false;
|
|
827
|
+
}
|
|
828
|
+
helpers.scheduleAutoSubmit();
|
|
814
829
|
return state2.api;
|
|
815
830
|
};
|
|
816
831
|
|
|
817
832
|
// src/api/reset.ts
|
|
818
833
|
var createReset = (state2, helpers) => () => {
|
|
819
|
-
if (!state2.element) return state2.api;
|
|
820
|
-
state2.
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
state2.attributes.error,
|
|
828
|
-
state2.attributes.filled,
|
|
829
|
-
"aria-invalid"
|
|
830
|
-
]);
|
|
831
|
-
helpers.syncVisualState();
|
|
832
|
-
helpers.notifySubscribers();
|
|
834
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
835
|
+
state2.isResetting = true;
|
|
836
|
+
try {
|
|
837
|
+
state2.element.reset();
|
|
838
|
+
} finally {
|
|
839
|
+
state2.isResetting = false;
|
|
840
|
+
}
|
|
841
|
+
helpers.applyReset();
|
|
833
842
|
return state2.api;
|
|
834
843
|
};
|
|
835
844
|
|
|
836
845
|
// src/api/validate.ts
|
|
837
846
|
var createValidate = (state2, helpers) => (fields) => {
|
|
847
|
+
if (state2.isDestroyed) return { isValid: true, errors: {} };
|
|
838
848
|
state2.isValidated = true;
|
|
839
|
-
helpers.syncVisualState(fields);
|
|
840
849
|
return helpers.validateNames(fields);
|
|
841
850
|
};
|
|
842
851
|
|
|
@@ -845,6 +854,7 @@ var createRevalidate = createValidate;
|
|
|
845
854
|
|
|
846
855
|
// src/api/set-errors.ts
|
|
847
856
|
var createSetErrors = (state2, helpers) => (fields) => {
|
|
857
|
+
if (state2.isDestroyed) return state2.api;
|
|
848
858
|
if (Array.isArray(fields)) {
|
|
849
859
|
state2.manualErrors = {
|
|
850
860
|
...state2.manualErrors,
|
|
@@ -863,6 +873,7 @@ var createSetErrors = (state2, helpers) => (fields) => {
|
|
|
863
873
|
|
|
864
874
|
// src/api/set-value.ts
|
|
865
875
|
var createSetValue = (state2, helpers) => (name, value) => {
|
|
876
|
+
if (state2.isDestroyed) return state2.api;
|
|
866
877
|
const fields = helpers.getFieldsByName(name);
|
|
867
878
|
if (fields.length === 0) return state2.api;
|
|
868
879
|
writeFieldValue(fields, value);
|
|
@@ -874,6 +885,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
|
|
|
874
885
|
|
|
875
886
|
// src/api/subscribe.ts
|
|
876
887
|
var createSubscribe = (state2, _helpers) => (listener) => {
|
|
888
|
+
if (state2.isDestroyed) return () => void 0;
|
|
877
889
|
state2.subscribers.add(listener);
|
|
878
890
|
listener(state2.api.getState());
|
|
879
891
|
return () => {
|
|
@@ -1084,6 +1096,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1084
1096
|
submitCount: 0,
|
|
1085
1097
|
pendingSubmitCount: 0,
|
|
1086
1098
|
autoSubmitTimer: null,
|
|
1099
|
+
isAutoSubmitting: false,
|
|
1100
|
+
autoSubmitSuspended: false,
|
|
1101
|
+
isResetting: false,
|
|
1087
1102
|
isDestroyed: false,
|
|
1088
1103
|
listeners: [],
|
|
1089
1104
|
mutationObserver: null,
|
|
@@ -1128,12 +1143,20 @@ var createFormController = (target, options = {}) => {
|
|
|
1128
1143
|
state2.fieldCache.set(name, fields);
|
|
1129
1144
|
return fields;
|
|
1130
1145
|
};
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1146
|
+
const indexFieldsByName = () => {
|
|
1147
|
+
const index = /* @__PURE__ */ new Map();
|
|
1148
|
+
if (!state2.element) return index;
|
|
1134
1149
|
for (const field of getNamedFields(state2.element)) {
|
|
1135
|
-
|
|
1150
|
+
const bucket = index.get(field.name);
|
|
1151
|
+
if (bucket) bucket.push(field);
|
|
1152
|
+
else index.set(field.name, [field]);
|
|
1136
1153
|
}
|
|
1154
|
+
return index;
|
|
1155
|
+
};
|
|
1156
|
+
const getTrackedFieldNames = (index = indexFieldsByName()) => {
|
|
1157
|
+
if (!state2.element) return Object.keys(state2.validators);
|
|
1158
|
+
const names = new Set(Object.keys(state2.validators));
|
|
1159
|
+
for (const name of index.keys()) names.add(name);
|
|
1137
1160
|
for (const [canonical, entry] of state2.formattedFields) {
|
|
1138
1161
|
names.add(canonical);
|
|
1139
1162
|
if (entry.displayName && entry.displayName !== canonical) {
|
|
@@ -1144,8 +1167,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1144
1167
|
};
|
|
1145
1168
|
const getValues = () => {
|
|
1146
1169
|
const values = {};
|
|
1147
|
-
|
|
1148
|
-
|
|
1170
|
+
const index = indexFieldsByName();
|
|
1171
|
+
for (const name of getTrackedFieldNames(index)) {
|
|
1172
|
+
values[name] = readFieldValue(index.get(name) ?? []);
|
|
1149
1173
|
}
|
|
1150
1174
|
return values;
|
|
1151
1175
|
};
|
|
@@ -1238,29 +1262,54 @@ var createFormController = (target, options = {}) => {
|
|
|
1238
1262
|
const snapshot = state2.api.getState();
|
|
1239
1263
|
callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
|
|
1240
1264
|
};
|
|
1265
|
+
const runAutoSubmit = () => {
|
|
1266
|
+
state2.autoSubmitTimer = null;
|
|
1267
|
+
if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
|
|
1268
|
+
state2.isAutoSubmitting = true;
|
|
1269
|
+
try {
|
|
1270
|
+
submitForm(state2.element);
|
|
1271
|
+
} finally {
|
|
1272
|
+
state2.isAutoSubmitting = false;
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1241
1275
|
const scheduleAutoSubmit = () => {
|
|
1242
|
-
if (!state2.element || !state2.autoSubmitEnabled) return;
|
|
1276
|
+
if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
|
|
1243
1277
|
if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
|
|
1278
|
+
state2.autoSubmitTimer = null;
|
|
1244
1279
|
if (state2.autoSubmitDebounce > 0) {
|
|
1245
|
-
state2.autoSubmitTimer = setTimeout(
|
|
1246
|
-
() => submitForm(state2.element),
|
|
1247
|
-
state2.autoSubmitDebounce
|
|
1248
|
-
);
|
|
1280
|
+
state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
|
|
1249
1281
|
return;
|
|
1250
1282
|
}
|
|
1251
|
-
|
|
1283
|
+
runAutoSubmit();
|
|
1284
|
+
};
|
|
1285
|
+
const emitAllFieldWatchers = () => {
|
|
1286
|
+
for (const name of Array.from(state2.watchedFields.keys())) emitFieldWatchers(name);
|
|
1287
|
+
};
|
|
1288
|
+
const applyReset = () => {
|
|
1289
|
+
if (!state2.element || state2.isDestroyed) return;
|
|
1290
|
+
clearFieldCache();
|
|
1291
|
+
resetFormattedFields(state2);
|
|
1292
|
+
state2.manualErrors = {};
|
|
1293
|
+
state2.validationErrors = {};
|
|
1294
|
+
state2.validationGroupErrors = {};
|
|
1295
|
+
state2.validationElementErrors.clear();
|
|
1296
|
+
clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
|
|
1297
|
+
syncVisualState();
|
|
1298
|
+
emitAllFieldWatchers();
|
|
1299
|
+
notifySubscribers();
|
|
1252
1300
|
};
|
|
1253
1301
|
const helpers = {
|
|
1254
1302
|
notifySubscribers,
|
|
1255
1303
|
clearFieldCache,
|
|
1256
1304
|
getFieldsByName,
|
|
1257
|
-
getTrackedFieldNames,
|
|
1305
|
+
getTrackedFieldNames: () => getTrackedFieldNames(),
|
|
1258
1306
|
getValues,
|
|
1259
1307
|
getMergedErrors,
|
|
1260
1308
|
syncVisualState,
|
|
1261
1309
|
validateNames,
|
|
1262
1310
|
emitFieldWatchers,
|
|
1263
|
-
scheduleAutoSubmit
|
|
1311
|
+
scheduleAutoSubmit,
|
|
1312
|
+
applyReset
|
|
1264
1313
|
};
|
|
1265
1314
|
const handleDelegatedEvent = (event) => {
|
|
1266
1315
|
const target2 = event.target;
|
|
@@ -1289,6 +1338,23 @@ var createFormController = (target, options = {}) => {
|
|
|
1289
1338
|
if (target2.form !== state2.element || state2.element?.contains(target2)) return;
|
|
1290
1339
|
handleDelegatedEvent(event);
|
|
1291
1340
|
};
|
|
1341
|
+
const isFocusable = (candidate) => {
|
|
1342
|
+
if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
|
|
1343
|
+
return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
|
|
1344
|
+
};
|
|
1345
|
+
const focusFirstInvalidField = (errors) => {
|
|
1346
|
+
const candidates = [];
|
|
1347
|
+
for (const name of Object.keys(errors)) {
|
|
1348
|
+
const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
|
|
1349
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
1350
|
+
const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
|
|
1351
|
+
if (field) candidates.push(field);
|
|
1352
|
+
}
|
|
1353
|
+
candidates.sort(
|
|
1354
|
+
(left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
1355
|
+
);
|
|
1356
|
+
candidates[0]?.focus();
|
|
1357
|
+
};
|
|
1292
1358
|
const handleSubmitEvent = (event) => {
|
|
1293
1359
|
if (!state2.element || state2.isDestroyed || !state2.api) return;
|
|
1294
1360
|
if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
|
|
@@ -1299,23 +1365,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1299
1365
|
state2.submitCount += 1;
|
|
1300
1366
|
notifySubscribers();
|
|
1301
1367
|
if (!validation.isValid) {
|
|
1302
|
-
|
|
1303
|
-
const displayName = resolveDisplayNameForName(state2, name);
|
|
1304
|
-
const fields = getFieldsByName(displayName);
|
|
1305
|
-
const elementErrors = state2.validationElementErrors.get(name);
|
|
1306
|
-
const field = fields.find((candidate2) => {
|
|
1307
|
-
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1308
|
-
if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
|
|
1309
|
-
return elementErrors?.has(candidate2);
|
|
1310
|
-
}) ?? fields.find((candidate2) => {
|
|
1311
|
-
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1312
|
-
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
1313
|
-
});
|
|
1314
|
-
if (field) {
|
|
1315
|
-
field.focus();
|
|
1316
|
-
break;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1368
|
+
if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
|
|
1319
1369
|
return;
|
|
1320
1370
|
}
|
|
1321
1371
|
const candidate = event;
|
|
@@ -1328,7 +1378,13 @@ var createFormController = (target, options = {}) => {
|
|
|
1328
1378
|
try {
|
|
1329
1379
|
for (const handler of handlers) {
|
|
1330
1380
|
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
1331
|
-
if (result && typeof result.then === "function")
|
|
1381
|
+
if (result && typeof result.then === "function") {
|
|
1382
|
+
pending.push(
|
|
1383
|
+
Promise.resolve(result).catch((error) => {
|
|
1384
|
+
console.error("[forms] submit handler failed", error);
|
|
1385
|
+
})
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1332
1388
|
}
|
|
1333
1389
|
} catch (error) {
|
|
1334
1390
|
synchronousError = error;
|
|
@@ -1344,9 +1400,21 @@ var createFormController = (target, options = {}) => {
|
|
|
1344
1400
|
}
|
|
1345
1401
|
if (hasSynchronousError) throw synchronousError;
|
|
1346
1402
|
};
|
|
1403
|
+
const handleResetEvent = (event) => {
|
|
1404
|
+
if (state2.isResetting) return;
|
|
1405
|
+
const timer = setTimeout(() => {
|
|
1406
|
+
state2.cleanups.delete(cancel);
|
|
1407
|
+
if (!event.defaultPrevented) applyReset();
|
|
1408
|
+
}, 0);
|
|
1409
|
+
const cancel = () => clearTimeout(timer);
|
|
1410
|
+
state2.cleanups.add(cancel);
|
|
1411
|
+
};
|
|
1347
1412
|
const startMutationObserver = () => {
|
|
1348
1413
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
1349
|
-
|
|
1414
|
+
const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
|
|
1415
|
+
const isRelevant = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
|
|
1416
|
+
state2.mutationObserver = new MutationObserver((records) => {
|
|
1417
|
+
if (state2.isDestroyed || !records.some(isRelevant)) return;
|
|
1350
1418
|
clearFieldCache();
|
|
1351
1419
|
if (state2.isValidated) {
|
|
1352
1420
|
validateNames();
|
|
@@ -1399,6 +1467,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1399
1467
|
addListener2(state2.element, "input", handleDelegatedEvent);
|
|
1400
1468
|
addListener2(document, "input", handleExternalDelegatedEvent);
|
|
1401
1469
|
addListener2(state2.element, "submit", handleSubmitEvent);
|
|
1470
|
+
addListener2(state2.element, "reset", handleResetEvent);
|
|
1402
1471
|
startMutationObserver();
|
|
1403
1472
|
}
|
|
1404
1473
|
if (normalizedOptions.autoSubmit) {
|