@samline/forms 2.7.1 → 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 -6179
- 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.js
CHANGED
|
@@ -172,7 +172,7 @@ var validateValues = (values, schema) => {
|
|
|
172
172
|
|
|
173
173
|
// src/api/append.ts
|
|
174
174
|
var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
|
|
175
|
-
if (!state2.element) return null;
|
|
175
|
+
if (!state2.element || state2.isDestroyed) return null;
|
|
176
176
|
helpers.clearFieldCache();
|
|
177
177
|
if (className) {
|
|
178
178
|
const classSelector = className.trim().split(/\s+/)[0];
|
|
@@ -208,6 +208,7 @@ var createAddCleanup = (state2, _helpers) => (cleanup) => {
|
|
|
208
208
|
|
|
209
209
|
// src/api/auto-submit.ts
|
|
210
210
|
var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
211
|
+
if (state2.isDestroyed) return state2.api;
|
|
211
212
|
state2.autoSubmitEnabled = next !== false;
|
|
212
213
|
if (!state2.autoSubmitEnabled && state2.autoSubmitTimer) {
|
|
213
214
|
clearTimeout(state2.autoSubmitTimer);
|
|
@@ -220,6 +221,7 @@ var createAutoSubmit = (state2, helpers) => (next = true) => {
|
|
|
220
221
|
|
|
221
222
|
// src/api/clear-errors.ts
|
|
222
223
|
var createClearErrors = (state2, helpers) => (fields) => {
|
|
224
|
+
if (state2.isDestroyed) return state2.api;
|
|
223
225
|
if (!fields) {
|
|
224
226
|
state2.manualErrors = {};
|
|
225
227
|
helpers.syncVisualState();
|
|
@@ -346,6 +348,7 @@ var isFormatChar = (char) => {
|
|
|
346
348
|
return true;
|
|
347
349
|
};
|
|
348
350
|
var restoreCursor = (field, position) => {
|
|
351
|
+
if (field.ownerDocument.activeElement !== field) return;
|
|
349
352
|
const max = field.value.length;
|
|
350
353
|
const safe = clamp(position, 0, max);
|
|
351
354
|
const apply = () => {
|
|
@@ -709,7 +712,7 @@ var mergeErrors = (left, right) => {
|
|
|
709
712
|
var createGetState = (state2, helpers) => () => {
|
|
710
713
|
const values = helpers.getValues();
|
|
711
714
|
const errors = cloneErrors(helpers.getMergedErrors());
|
|
712
|
-
const filledFields =
|
|
715
|
+
const filledFields = Object.keys(values).filter((name) => {
|
|
713
716
|
const value = values[name];
|
|
714
717
|
if (Array.isArray(value)) return value.length > 0;
|
|
715
718
|
if (typeof value === "string") return value.trim().length > 0;
|
|
@@ -882,6 +885,7 @@ var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFi
|
|
|
882
885
|
|
|
883
886
|
// src/api/observe.ts
|
|
884
887
|
var createObserve = (state2, helpers) => (field, callback) => {
|
|
888
|
+
if (state2.isDestroyed) return () => void 0;
|
|
885
889
|
const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
|
|
886
890
|
callbacks.add(callback);
|
|
887
891
|
state2.watchedFields.set(field, callbacks);
|
|
@@ -899,44 +903,49 @@ var createObserve = (state2, helpers) => (field, callback) => {
|
|
|
899
903
|
|
|
900
904
|
// src/api/on-submit.ts
|
|
901
905
|
var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
|
|
906
|
+
if (state2.isDestroyed) return state2.api;
|
|
902
907
|
state2.submitHandlers.add({ callback, preventDefault });
|
|
903
908
|
return state2.api;
|
|
904
909
|
};
|
|
905
910
|
|
|
906
911
|
// src/api/prefill.ts
|
|
907
|
-
var createPrefill = (state2,
|
|
908
|
-
if (!state2.element) return state2.api;
|
|
912
|
+
var createPrefill = (state2, helpers) => (fieldName) => {
|
|
913
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
909
914
|
const params = new URLSearchParams(window.location.search);
|
|
910
|
-
params.
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
915
|
+
const keys = Array.from(new Set(params.keys())).filter(
|
|
916
|
+
(key) => !fieldName || fieldName === key
|
|
917
|
+
);
|
|
918
|
+
if (keys.length === 0) return state2.api;
|
|
919
|
+
state2.autoSubmitSuspended = true;
|
|
920
|
+
try {
|
|
921
|
+
for (const key of keys) {
|
|
922
|
+
const values = params.getAll(key);
|
|
923
|
+
state2.api.setValue(key, values.length > 1 ? values : values[0]);
|
|
924
|
+
}
|
|
925
|
+
} finally {
|
|
926
|
+
state2.autoSubmitSuspended = false;
|
|
927
|
+
}
|
|
928
|
+
helpers.scheduleAutoSubmit();
|
|
914
929
|
return state2.api;
|
|
915
930
|
};
|
|
916
931
|
|
|
917
932
|
// src/api/reset.ts
|
|
918
933
|
var createReset = (state2, helpers) => () => {
|
|
919
|
-
if (!state2.element) return state2.api;
|
|
920
|
-
state2.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
state2.attributes.error,
|
|
928
|
-
state2.attributes.filled,
|
|
929
|
-
"aria-invalid"
|
|
930
|
-
]);
|
|
931
|
-
helpers.syncVisualState();
|
|
932
|
-
helpers.notifySubscribers();
|
|
934
|
+
if (!state2.element || state2.isDestroyed) return state2.api;
|
|
935
|
+
state2.isResetting = true;
|
|
936
|
+
try {
|
|
937
|
+
state2.element.reset();
|
|
938
|
+
} finally {
|
|
939
|
+
state2.isResetting = false;
|
|
940
|
+
}
|
|
941
|
+
helpers.applyReset();
|
|
933
942
|
return state2.api;
|
|
934
943
|
};
|
|
935
944
|
|
|
936
945
|
// src/api/validate.ts
|
|
937
946
|
var createValidate = (state2, helpers) => (fields) => {
|
|
947
|
+
if (state2.isDestroyed) return { isValid: true, errors: {} };
|
|
938
948
|
state2.isValidated = true;
|
|
939
|
-
helpers.syncVisualState(fields);
|
|
940
949
|
return helpers.validateNames(fields);
|
|
941
950
|
};
|
|
942
951
|
|
|
@@ -945,6 +954,7 @@ var createRevalidate = createValidate;
|
|
|
945
954
|
|
|
946
955
|
// src/api/set-errors.ts
|
|
947
956
|
var createSetErrors = (state2, helpers) => (fields) => {
|
|
957
|
+
if (state2.isDestroyed) return state2.api;
|
|
948
958
|
if (Array.isArray(fields)) {
|
|
949
959
|
state2.manualErrors = {
|
|
950
960
|
...state2.manualErrors,
|
|
@@ -963,6 +973,7 @@ var createSetErrors = (state2, helpers) => (fields) => {
|
|
|
963
973
|
|
|
964
974
|
// src/api/set-value.ts
|
|
965
975
|
var createSetValue = (state2, helpers) => (name, value) => {
|
|
976
|
+
if (state2.isDestroyed) return state2.api;
|
|
966
977
|
const fields = helpers.getFieldsByName(name);
|
|
967
978
|
if (fields.length === 0) return state2.api;
|
|
968
979
|
writeFieldValue(fields, value);
|
|
@@ -974,6 +985,7 @@ var createSetValue = (state2, helpers) => (name, value) => {
|
|
|
974
985
|
|
|
975
986
|
// src/api/subscribe.ts
|
|
976
987
|
var createSubscribe = (state2, _helpers) => (listener) => {
|
|
988
|
+
if (state2.isDestroyed) return () => void 0;
|
|
977
989
|
state2.subscribers.add(listener);
|
|
978
990
|
listener(state2.api.getState());
|
|
979
991
|
return () => {
|
|
@@ -1043,6 +1055,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1043
1055
|
submitCount: 0,
|
|
1044
1056
|
pendingSubmitCount: 0,
|
|
1045
1057
|
autoSubmitTimer: null,
|
|
1058
|
+
isAutoSubmitting: false,
|
|
1059
|
+
autoSubmitSuspended: false,
|
|
1060
|
+
isResetting: false,
|
|
1046
1061
|
isDestroyed: false,
|
|
1047
1062
|
listeners: [],
|
|
1048
1063
|
mutationObserver: null,
|
|
@@ -1087,12 +1102,20 @@ var createFormController = (target, options = {}) => {
|
|
|
1087
1102
|
state2.fieldCache.set(name, fields);
|
|
1088
1103
|
return fields;
|
|
1089
1104
|
};
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1105
|
+
const indexFieldsByName = () => {
|
|
1106
|
+
const index = /* @__PURE__ */ new Map();
|
|
1107
|
+
if (!state2.element) return index;
|
|
1093
1108
|
for (const field of getNamedFields(state2.element)) {
|
|
1094
|
-
|
|
1109
|
+
const bucket = index.get(field.name);
|
|
1110
|
+
if (bucket) bucket.push(field);
|
|
1111
|
+
else index.set(field.name, [field]);
|
|
1095
1112
|
}
|
|
1113
|
+
return index;
|
|
1114
|
+
};
|
|
1115
|
+
const getTrackedFieldNames = (index = indexFieldsByName()) => {
|
|
1116
|
+
if (!state2.element) return Object.keys(state2.validators);
|
|
1117
|
+
const names = new Set(Object.keys(state2.validators));
|
|
1118
|
+
for (const name of index.keys()) names.add(name);
|
|
1096
1119
|
for (const [canonical, entry] of state2.formattedFields) {
|
|
1097
1120
|
names.add(canonical);
|
|
1098
1121
|
if (entry.displayName && entry.displayName !== canonical) {
|
|
@@ -1103,8 +1126,9 @@ var createFormController = (target, options = {}) => {
|
|
|
1103
1126
|
};
|
|
1104
1127
|
const getValues = () => {
|
|
1105
1128
|
const values = {};
|
|
1106
|
-
|
|
1107
|
-
|
|
1129
|
+
const index = indexFieldsByName();
|
|
1130
|
+
for (const name of getTrackedFieldNames(index)) {
|
|
1131
|
+
values[name] = readFieldValue(index.get(name) ?? []);
|
|
1108
1132
|
}
|
|
1109
1133
|
return values;
|
|
1110
1134
|
};
|
|
@@ -1197,29 +1221,54 @@ var createFormController = (target, options = {}) => {
|
|
|
1197
1221
|
const snapshot = state2.api.getState();
|
|
1198
1222
|
callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
|
|
1199
1223
|
};
|
|
1224
|
+
const runAutoSubmit = () => {
|
|
1225
|
+
state2.autoSubmitTimer = null;
|
|
1226
|
+
if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
|
|
1227
|
+
state2.isAutoSubmitting = true;
|
|
1228
|
+
try {
|
|
1229
|
+
submitForm(state2.element);
|
|
1230
|
+
} finally {
|
|
1231
|
+
state2.isAutoSubmitting = false;
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1200
1234
|
const scheduleAutoSubmit = () => {
|
|
1201
|
-
if (!state2.element || !state2.autoSubmitEnabled) return;
|
|
1235
|
+
if (!state2.element || !state2.autoSubmitEnabled || state2.autoSubmitSuspended) return;
|
|
1202
1236
|
if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
|
|
1237
|
+
state2.autoSubmitTimer = null;
|
|
1203
1238
|
if (state2.autoSubmitDebounce > 0) {
|
|
1204
|
-
state2.autoSubmitTimer = setTimeout(
|
|
1205
|
-
() => submitForm(state2.element),
|
|
1206
|
-
state2.autoSubmitDebounce
|
|
1207
|
-
);
|
|
1239
|
+
state2.autoSubmitTimer = setTimeout(runAutoSubmit, state2.autoSubmitDebounce);
|
|
1208
1240
|
return;
|
|
1209
1241
|
}
|
|
1210
|
-
|
|
1242
|
+
runAutoSubmit();
|
|
1243
|
+
};
|
|
1244
|
+
const emitAllFieldWatchers = () => {
|
|
1245
|
+
for (const name of Array.from(state2.watchedFields.keys())) emitFieldWatchers(name);
|
|
1246
|
+
};
|
|
1247
|
+
const applyReset = () => {
|
|
1248
|
+
if (!state2.element || state2.isDestroyed) return;
|
|
1249
|
+
clearFieldCache();
|
|
1250
|
+
resetFormattedFields(state2);
|
|
1251
|
+
state2.manualErrors = {};
|
|
1252
|
+
state2.validationErrors = {};
|
|
1253
|
+
state2.validationGroupErrors = {};
|
|
1254
|
+
state2.validationElementErrors.clear();
|
|
1255
|
+
clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
|
|
1256
|
+
syncVisualState();
|
|
1257
|
+
emitAllFieldWatchers();
|
|
1258
|
+
notifySubscribers();
|
|
1211
1259
|
};
|
|
1212
1260
|
const helpers = {
|
|
1213
1261
|
notifySubscribers,
|
|
1214
1262
|
clearFieldCache,
|
|
1215
1263
|
getFieldsByName,
|
|
1216
|
-
getTrackedFieldNames,
|
|
1264
|
+
getTrackedFieldNames: () => getTrackedFieldNames(),
|
|
1217
1265
|
getValues,
|
|
1218
1266
|
getMergedErrors,
|
|
1219
1267
|
syncVisualState,
|
|
1220
1268
|
validateNames,
|
|
1221
1269
|
emitFieldWatchers,
|
|
1222
|
-
scheduleAutoSubmit
|
|
1270
|
+
scheduleAutoSubmit,
|
|
1271
|
+
applyReset
|
|
1223
1272
|
};
|
|
1224
1273
|
const handleDelegatedEvent = (event) => {
|
|
1225
1274
|
const target2 = event.target;
|
|
@@ -1248,6 +1297,23 @@ var createFormController = (target, options = {}) => {
|
|
|
1248
1297
|
if (target2.form !== state2.element || state2.element?.contains(target2)) return;
|
|
1249
1298
|
handleDelegatedEvent(event);
|
|
1250
1299
|
};
|
|
1300
|
+
const isFocusable = (candidate) => {
|
|
1301
|
+
if (!candidate.isConnected || candidate.disabled || candidate.hidden) return false;
|
|
1302
|
+
return !(candidate instanceof HTMLInputElement && candidate.type === "hidden");
|
|
1303
|
+
};
|
|
1304
|
+
const focusFirstInvalidField = (errors) => {
|
|
1305
|
+
const candidates = [];
|
|
1306
|
+
for (const name of Object.keys(errors)) {
|
|
1307
|
+
const fields = getFieldsByName(resolveDisplayNameForName(state2, name)).filter(isFocusable);
|
|
1308
|
+
const elementErrors = state2.validationElementErrors.get(name);
|
|
1309
|
+
const field = fields.find((candidate) => elementErrors?.has(candidate)) ?? fields[0];
|
|
1310
|
+
if (field) candidates.push(field);
|
|
1311
|
+
}
|
|
1312
|
+
candidates.sort(
|
|
1313
|
+
(left, right) => left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
1314
|
+
);
|
|
1315
|
+
candidates[0]?.focus();
|
|
1316
|
+
};
|
|
1251
1317
|
const handleSubmitEvent = (event) => {
|
|
1252
1318
|
if (!state2.element || state2.isDestroyed || !state2.api) return;
|
|
1253
1319
|
if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
|
|
@@ -1258,23 +1324,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1258
1324
|
state2.submitCount += 1;
|
|
1259
1325
|
notifySubscribers();
|
|
1260
1326
|
if (!validation.isValid) {
|
|
1261
|
-
|
|
1262
|
-
const displayName = resolveDisplayNameForName(state2, name);
|
|
1263
|
-
const fields = getFieldsByName(displayName);
|
|
1264
|
-
const elementErrors = state2.validationElementErrors.get(name);
|
|
1265
|
-
const field = fields.find((candidate2) => {
|
|
1266
|
-
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1267
|
-
if (candidate2 instanceof HTMLInputElement && candidate2.type === "hidden") return false;
|
|
1268
|
-
return elementErrors?.has(candidate2);
|
|
1269
|
-
}) ?? fields.find((candidate2) => {
|
|
1270
|
-
if (!candidate2.isConnected || candidate2.disabled || candidate2.hidden) return false;
|
|
1271
|
-
return !(candidate2 instanceof HTMLInputElement && candidate2.type === "hidden");
|
|
1272
|
-
});
|
|
1273
|
-
if (field) {
|
|
1274
|
-
field.focus();
|
|
1275
|
-
break;
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1327
|
+
if (!state2.isAutoSubmitting) focusFirstInvalidField(validation.errors);
|
|
1278
1328
|
return;
|
|
1279
1329
|
}
|
|
1280
1330
|
const candidate = event;
|
|
@@ -1287,7 +1337,13 @@ var createFormController = (target, options = {}) => {
|
|
|
1287
1337
|
try {
|
|
1288
1338
|
for (const handler of handlers) {
|
|
1289
1339
|
const result = handler.callback(state2.element, data, formData, snapshot);
|
|
1290
|
-
if (result && typeof result.then === "function")
|
|
1340
|
+
if (result && typeof result.then === "function") {
|
|
1341
|
+
pending.push(
|
|
1342
|
+
Promise.resolve(result).catch((error) => {
|
|
1343
|
+
console.error("[forms] submit handler failed", error);
|
|
1344
|
+
})
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1291
1347
|
}
|
|
1292
1348
|
} catch (error) {
|
|
1293
1349
|
synchronousError = error;
|
|
@@ -1303,9 +1359,21 @@ var createFormController = (target, options = {}) => {
|
|
|
1303
1359
|
}
|
|
1304
1360
|
if (hasSynchronousError) throw synchronousError;
|
|
1305
1361
|
};
|
|
1362
|
+
const handleResetEvent = (event) => {
|
|
1363
|
+
if (state2.isResetting) return;
|
|
1364
|
+
const timer = setTimeout(() => {
|
|
1365
|
+
state2.cleanups.delete(cancel);
|
|
1366
|
+
if (!event.defaultPrevented) applyReset();
|
|
1367
|
+
}, 0);
|
|
1368
|
+
const cancel = () => clearTimeout(timer);
|
|
1369
|
+
state2.cleanups.add(cancel);
|
|
1370
|
+
};
|
|
1306
1371
|
const startMutationObserver = () => {
|
|
1307
1372
|
if (!state2.element || typeof MutationObserver === "undefined") return;
|
|
1308
|
-
|
|
1373
|
+
const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
|
|
1374
|
+
const isRelevant = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
|
|
1375
|
+
state2.mutationObserver = new MutationObserver((records) => {
|
|
1376
|
+
if (state2.isDestroyed || !records.some(isRelevant)) return;
|
|
1309
1377
|
clearFieldCache();
|
|
1310
1378
|
if (state2.isValidated) {
|
|
1311
1379
|
validateNames();
|
|
@@ -1358,6 +1426,7 @@ var createFormController = (target, options = {}) => {
|
|
|
1358
1426
|
addListener2(state2.element, "input", handleDelegatedEvent);
|
|
1359
1427
|
addListener2(document, "input", handleExternalDelegatedEvent);
|
|
1360
1428
|
addListener2(state2.element, "submit", handleSubmitEvent);
|
|
1429
|
+
addListener2(state2.element, "reset", handleResetEvent);
|
|
1361
1430
|
startMutationObserver();
|
|
1362
1431
|
}
|
|
1363
1432
|
if (normalizedOptions.autoSubmit) {
|
|
@@ -1417,4 +1486,4 @@ export {
|
|
|
1417
1486
|
form,
|
|
1418
1487
|
browser
|
|
1419
1488
|
};
|
|
1420
|
-
//# sourceMappingURL=chunk-
|
|
1489
|
+
//# sourceMappingURL=chunk-23VZAYA3.js.map
|