@samline/forms 1.0.2 → 2.0.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.
Files changed (49) hide show
  1. package/README.md +88 -186
  2. package/dist/browser/global.d.ts +152 -0
  3. package/dist/browser/global.global.js +780 -472
  4. package/dist/browser/global.global.js.map +1 -1
  5. package/dist/index.cjs +755 -468
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +141 -3
  8. package/dist/index.d.ts +141 -3
  9. package/dist/index.js +960 -13
  10. package/dist/index.js.map +1 -1
  11. package/package.json +8 -43
  12. package/dist/chunk-4NEGCRY2.js +0 -125
  13. package/dist/chunk-4NEGCRY2.js.map +0 -1
  14. package/dist/chunk-KIJEFIHP.js +0 -572
  15. package/dist/chunk-KIJEFIHP.js.map +0 -1
  16. package/dist/chunk-KKQD25KI.js +0 -11
  17. package/dist/chunk-KKQD25KI.js.map +0 -1
  18. package/dist/core/index.cjs +0 -154
  19. package/dist/core/index.cjs.map +0 -1
  20. package/dist/core/index.d.cts +0 -11
  21. package/dist/core/index.d.ts +0 -11
  22. package/dist/core/index.js +0 -13
  23. package/dist/core/index.js.map +0 -1
  24. package/dist/react/index.cjs +0 -730
  25. package/dist/react/index.cjs.map +0 -1
  26. package/dist/react/index.d.cts +0 -10
  27. package/dist/react/index.d.ts +0 -10
  28. package/dist/react/index.js +0 -43
  29. package/dist/react/index.js.map +0 -1
  30. package/dist/svelte/index.cjs +0 -760
  31. package/dist/svelte/index.cjs.map +0 -1
  32. package/dist/svelte/index.d.cts +0 -20
  33. package/dist/svelte/index.d.ts +0 -20
  34. package/dist/svelte/index.js +0 -72
  35. package/dist/svelte/index.js.map +0 -1
  36. package/dist/types-C0_1rYlm.d.cts +0 -94
  37. package/dist/types-C0_1rYlm.d.ts +0 -94
  38. package/dist/vanilla/index.cjs +0 -701
  39. package/dist/vanilla/index.cjs.map +0 -1
  40. package/dist/vanilla/index.d.cts +0 -7
  41. package/dist/vanilla/index.d.ts +0 -7
  42. package/dist/vanilla/index.js +0 -12
  43. package/dist/vanilla/index.js.map +0 -1
  44. package/dist/vue/index.cjs +0 -737
  45. package/dist/vue/index.cjs.map +0 -1
  46. package/dist/vue/index.d.cts +0 -13
  47. package/dist/vue/index.d.ts +0 -13
  48. package/dist/vue/index.js +0 -56
  49. package/dist/vue/index.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,12 +17,19 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var src_exports = {};
22
32
  __export(src_exports, {
23
- createEmptyFormState: () => createEmptyFormState,
24
33
  createFormController: () => createFormController,
25
34
  form: () => form,
26
35
  parseFormData: () => parseFormData,
@@ -29,6 +38,343 @@ __export(src_exports, {
29
38
  });
30
39
  module.exports = __toCommonJS(src_exports);
31
40
 
41
+ // src/api/append.ts
42
+ var createAppend = (state2, helpers) => ({ tag, content, class: className, atStart = false }) => {
43
+ if (!state2.element) return null;
44
+ helpers.clearFieldCache();
45
+ if (className) {
46
+ const classSelector = className.trim().split(/\s+/)[0];
47
+ const existing = classSelector ? state2.element.querySelector(`.${classSelector}`) : null;
48
+ existing?.remove();
49
+ }
50
+ const node = document.createElement(tag);
51
+ if (className) node.className = className;
52
+ node.innerHTML = content;
53
+ if (atStart && state2.element.firstChild) {
54
+ state2.element.insertBefore(node, state2.element.firstChild);
55
+ } else {
56
+ state2.element.appendChild(node);
57
+ }
58
+ return node;
59
+ };
60
+
61
+ // src/api/auto-submit.ts
62
+ var createAutoSubmit = (state2, helpers) => (next = true) => {
63
+ state2.autoSubmitEnabled = next !== false;
64
+ state2.autoSubmitDebounce = typeof next === "object" && next ? next.debounce ?? 0 : 0;
65
+ helpers.notifySubscribers();
66
+ return state2.api;
67
+ };
68
+
69
+ // src/api/clear-errors.ts
70
+ var createClearErrors = (state2, helpers) => (fields) => {
71
+ if (!fields) {
72
+ state2.manualErrors = {};
73
+ helpers.syncVisualState();
74
+ helpers.notifySubscribers();
75
+ return state2.api;
76
+ }
77
+ for (const field of fields) {
78
+ delete state2.manualErrors[field];
79
+ }
80
+ helpers.syncVisualState(fields);
81
+ helpers.notifySubscribers();
82
+ return state2.api;
83
+ };
84
+
85
+ // src/core/format-helpers.ts
86
+ var FORMATTER_RAW_ATTRIBUTE = "data-formatter-raw-for";
87
+ var cssEscape = (value) => {
88
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
89
+ return CSS.escape(value);
90
+ }
91
+ return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
92
+ };
93
+ var findRawMirror = (form2, fieldName) => {
94
+ const byAttr = form2.querySelector(
95
+ `[${FORMATTER_RAW_ATTRIBUTE}="${cssEscape(fieldName)}"]`
96
+ );
97
+ if (byAttr) return byAttr;
98
+ const byName = form2.querySelector(
99
+ `[name="${cssEscape(`${fieldName}_raw`)}"]`
100
+ );
101
+ if (byName) return byName;
102
+ return null;
103
+ };
104
+ var ensureRawMirror = (form2, fieldName) => {
105
+ const existing = findRawMirror(form2, fieldName);
106
+ if (existing) return existing;
107
+ const input = document.createElement("input");
108
+ input.type = "hidden";
109
+ input.name = `${fieldName}Raw`;
110
+ input.setAttribute("aria-hidden", "true");
111
+ input.tabIndex = -1;
112
+ input.setAttribute(FORMATTER_RAW_ATTRIBUTE, fieldName);
113
+ form2.appendChild(input);
114
+ return input;
115
+ };
116
+ var applyFormattedValue = (visible, mirror, formatted, raw) => {
117
+ const rawInput = visible.value;
118
+ const caret = clamp(
119
+ visible.selectionStart ?? rawInput.length,
120
+ 0,
121
+ rawInput.length
122
+ );
123
+ if (rawInput === formatted) {
124
+ if (mirror && mirror.value !== raw) mirror.value = raw;
125
+ return caret;
126
+ }
127
+ visible.value = formatted;
128
+ if (mirror) mirror.value = raw;
129
+ const newCaret = computeCursorPosition(caret, rawInput, formatted);
130
+ restoreCursor(visible, newCaret);
131
+ return newCaret;
132
+ };
133
+ var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
134
+ var computeCursorPosition = (caretPos, rawInput, newValue) => {
135
+ if (newValue.length === 0) return 0;
136
+ if (caretPos >= rawInput.length) return newValue.length;
137
+ const targetNonFormat = countNonFormat(rawInput.slice(0, caretPos));
138
+ if (targetNonFormat === 0) {
139
+ let i = 0;
140
+ while (i < newValue.length && isFormatChar(newValue[i])) i += 1;
141
+ return i;
142
+ }
143
+ let seen = 0;
144
+ for (let i = 0; i < newValue.length; i += 1) {
145
+ if (!isFormatChar(newValue[i])) {
146
+ seen += 1;
147
+ if (seen === targetNonFormat) {
148
+ return Math.min(i + 1, newValue.length);
149
+ }
150
+ }
151
+ }
152
+ return newValue.length;
153
+ };
154
+ var countNonFormat = (value) => {
155
+ let count = 0;
156
+ for (const char of value) {
157
+ if (!isFormatChar(char)) count += 1;
158
+ }
159
+ return count;
160
+ };
161
+ var isFormatChar = (char) => {
162
+ if (!char) return false;
163
+ if (char >= "0" && char <= "9") return false;
164
+ if (char >= "a" && char <= "z" || char >= "A" && char <= "Z") return false;
165
+ return true;
166
+ };
167
+ var restoreCursor = (field, position) => {
168
+ const max = field.value.length;
169
+ const safe = clamp(position, 0, max);
170
+ const apply = () => {
171
+ try {
172
+ field.setSelectionRange(safe, safe);
173
+ } catch {
174
+ }
175
+ };
176
+ if (typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent)) {
177
+ if (typeof requestAnimationFrame === "function") {
178
+ requestAnimationFrame(apply);
179
+ } else {
180
+ setTimeout(apply, 1);
181
+ }
182
+ return;
183
+ }
184
+ apply();
185
+ };
186
+
187
+ // src/core/formatter-loader.ts
188
+ var state = null;
189
+ var MISSING_MESSAGE = "[samline/forms] The `format()` and `formatAll()` methods require the @samline/formatter package. Install it with: npm i @samline/formatter (or pnpm/bun/yarn equivalent).";
190
+ var warned = false;
191
+ var warnOnce = () => {
192
+ if (warned) return;
193
+ warned = true;
194
+ console.error(MISSING_MESSAGE);
195
+ };
196
+ var loadFormatter = () => {
197
+ if (state && state.status === "resolved") {
198
+ if (!state.module) warnOnce();
199
+ return Promise.resolve(state.module);
200
+ }
201
+ if (state && state.status === "pending") {
202
+ return state.promise;
203
+ }
204
+ const promise = (async () => {
205
+ try {
206
+ const mod = await import(
207
+ /* @vite-ignore */
208
+ "@samline/formatter"
209
+ );
210
+ state = { status: "resolved", module: mod };
211
+ return mod;
212
+ } catch {
213
+ warnOnce();
214
+ state = { status: "resolved", module: null };
215
+ return null;
216
+ }
217
+ })();
218
+ state = { status: "pending", promise };
219
+ return promise;
220
+ };
221
+
222
+ // src/api/format.ts
223
+ var registry = /* @__PURE__ */ new WeakMap();
224
+ var getRegistry = (state2) => {
225
+ let bucket = registry.get(state2);
226
+ if (!bucket) {
227
+ bucket = /* @__PURE__ */ new Map();
228
+ registry.set(state2, bucket);
229
+ }
230
+ return bucket;
231
+ };
232
+ var resolveFieldNames = (config) => Array.isArray(config.field) ? config.field : [config.field];
233
+ var addListener = (state2, target, type, handler) => {
234
+ target.addEventListener(type, handler);
235
+ state2.listeners.push({ element: target, type, handler });
236
+ };
237
+ var buildHandler = (field, mirror, formatFn, formatType, formatOptions, mirrorFieldName) => {
238
+ const handler = (event) => {
239
+ if (!field.isConnected) return;
240
+ const rawInput = event.target.value;
241
+ const { formatted, raw } = formatFn(rawInput, formatType, formatOptions);
242
+ if (!formatted && !raw) {
243
+ field.value = "";
244
+ if (mirror && mirror.value !== "") mirror.value = "";
245
+ return;
246
+ }
247
+ applyFormattedValue(field, mirror, formatted, raw);
248
+ void mirrorFieldName;
249
+ };
250
+ return handler;
251
+ };
252
+ var applyFormat = async (state2, helpers, config) => {
253
+ if (!state2.element || !state2.api) return;
254
+ const formatter = await loadFormatter();
255
+ if (!formatter) return;
256
+ const formatType = config.type;
257
+ const formatOptions = config.options;
258
+ const fieldNames = resolveFieldNames(config);
259
+ const bucket = getRegistry(state2);
260
+ for (const fieldName of fieldNames) {
261
+ const fields = helpers.getFieldsByName(fieldName);
262
+ if (fields.length === 0) continue;
263
+ const writable = fields.filter(
264
+ (f) => f instanceof HTMLInputElement && f.type !== "checkbox" && f.type !== "radio" && f.type !== "file" && f.type !== "submit" && f.type !== "button" || f instanceof HTMLTextAreaElement
265
+ );
266
+ if (writable.length === 0) continue;
267
+ for (const field of writable) {
268
+ const existing = bucket.get(fieldName);
269
+ const mirrorName = config.rawField ?? `${fieldName}Raw`;
270
+ let mirror = findRawMirror(state2.element, mirrorName) ?? null;
271
+ const mirrorIsOwned = !mirror;
272
+ if (!mirror) mirror = ensureRawMirror(state2.element, fieldName);
273
+ if (mirror && mirror.name !== mirrorName) mirror.name = mirrorName;
274
+ const trackerMirrorName = mirror?.name ?? mirrorName;
275
+ if (existing) {
276
+ existing.mirrorName = trackerMirrorName;
277
+ existing.mirrorIsOwned = mirrorIsOwned;
278
+ state2.formattedFields.set(fieldName, {
279
+ config,
280
+ mirrorName: trackerMirrorName,
281
+ mirrorIsOwned
282
+ });
283
+ continue;
284
+ }
285
+ const handler = buildHandler(
286
+ field,
287
+ mirror,
288
+ formatter.format,
289
+ formatType,
290
+ formatOptions,
291
+ trackerMirrorName
292
+ );
293
+ addListener(state2, state2.element, "input", handler);
294
+ bucket.set(fieldName, {
295
+ fields: /* @__PURE__ */ new Set([field]),
296
+ mirrorName: trackerMirrorName,
297
+ handler,
298
+ mirrorIsOwned
299
+ });
300
+ state2.formattedFields.set(fieldName, {
301
+ config,
302
+ mirrorName: trackerMirrorName,
303
+ mirrorIsOwned
304
+ });
305
+ const initial = field.value;
306
+ if (initial !== "") {
307
+ const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
308
+ applyFormattedValue(field, mirror, formatted, raw);
309
+ } else if (mirror && mirror.value !== "") {
310
+ mirror.value = "";
311
+ }
312
+ }
313
+ }
314
+ };
315
+ var createFormat = (state2, helpers) => (config) => {
316
+ if (state2.isDestroyed) return state2.api;
317
+ if (!state2.element) return state2.api;
318
+ void applyFormat(state2, helpers, config);
319
+ return state2.api;
320
+ };
321
+ var createFormatAll = createFormat;
322
+ var cleanupFormatRegistry = (state2) => {
323
+ if (!state2.element) {
324
+ registry.delete(state2);
325
+ return;
326
+ }
327
+ const bucket = registry.get(state2);
328
+ if (!bucket) {
329
+ state2.element.querySelectorAll(`[${FORMATTER_RAW_ATTRIBUTE}]`).forEach((node) => node.remove());
330
+ return;
331
+ }
332
+ for (const [fieldName, entry] of bucket) {
333
+ state2.element.removeEventListener("input", entry.handler);
334
+ if (entry.mirrorIsOwned) {
335
+ const mirror = findRawMirror(state2.element, entry.mirrorName);
336
+ mirror?.remove();
337
+ }
338
+ bucket.delete(fieldName);
339
+ state2.formattedFields.delete(fieldName);
340
+ }
341
+ state2.element.querySelectorAll(`[${FORMATTER_RAW_ATTRIBUTE}]`).forEach((node) => node.remove());
342
+ };
343
+
344
+ // src/api/destroy.ts
345
+ var createDestroy = (state2, _helpers) => () => {
346
+ if (state2.isDestroyed) return;
347
+ state2.isDestroyed = true;
348
+ for (const listener of state2.listeners) {
349
+ listener.element.removeEventListener(listener.type, listener.handler);
350
+ }
351
+ state2.listeners.length = 0;
352
+ if (state2.autoSubmitTimer) {
353
+ clearTimeout(state2.autoSubmitTimer);
354
+ state2.autoSubmitTimer = null;
355
+ }
356
+ state2.mutationObserver?.disconnect();
357
+ state2.mutationObserver = null;
358
+ state2.watchedFields.clear();
359
+ state2.subscribers.clear();
360
+ state2.submitHandlers.clear();
361
+ state2.fieldCache.clear();
362
+ state2.manualErrors = {};
363
+ state2.validationErrors = {};
364
+ cleanupFormatRegistry(state2);
365
+ };
366
+
367
+ // src/api/disable-auto-submit.ts
368
+ var createDisableAutoSubmit = (state2, helpers) => () => {
369
+ state2.autoSubmitEnabled = false;
370
+ if (state2.autoSubmitTimer) {
371
+ clearTimeout(state2.autoSubmitTimer);
372
+ state2.autoSubmitTimer = null;
373
+ }
374
+ helpers.notifySubscribers();
375
+ return state2.api;
376
+ };
377
+
32
378
  // src/core/serialize.ts
33
379
  var isEmptyFile = (value) => value instanceof File && value.size === 0 && value.name === "";
34
380
  var appendValue = (data, key, value) => {
@@ -44,19 +390,30 @@ var appendValue = (data, key, value) => {
44
390
  data[key] = [current, value];
45
391
  };
46
392
  var parseFormData = (formElement) => {
47
- const rawFormData = new FormData(formElement);
393
+ const raw = new FormData(formElement);
48
394
  const formData = new FormData();
49
395
  const data = {};
50
- rawFormData.forEach((value, key) => {
51
- if (isEmptyFile(value)) {
52
- return;
53
- }
396
+ raw.forEach((value, key) => {
397
+ if (isEmptyFile(value)) return;
54
398
  formData.append(key, value);
55
399
  appendValue(data, key, value);
56
400
  });
57
401
  return { data, formData };
58
402
  };
59
403
 
404
+ // src/api/get-data.ts
405
+ var createGetData = (_state, _helpers) => () => {
406
+ if (!_state.element) return { data: {}, formData: new FormData() };
407
+ return parseFormData(_state.element);
408
+ };
409
+
410
+ // src/api/get-field.ts
411
+ var createGetField = (state2, helpers) => (name) => {
412
+ const fields = helpers.getFieldsByName(name);
413
+ if (fields.length === 0) return null;
414
+ return fields.length === 1 ? fields[0] : fields;
415
+ };
416
+
60
417
  // src/core/state.ts
61
418
  var createEmptyFormState = () => ({
62
419
  values: {},
@@ -67,98 +424,47 @@ var createEmptyFormState = () => ({
67
424
  autoSubmit: false,
68
425
  submitCount: 0
69
426
  });
70
-
71
- // src/core/validation.ts
72
- var resolveRule = (rule) => {
73
- if (rule === void 0) {
74
- return { value: void 0, message: void 0 };
75
- }
76
- if (typeof rule === "object" && rule !== null && "value" in rule) {
77
- return { value: rule.value, message: rule.message };
78
- }
79
- return { value: rule, message: void 0 };
80
- };
81
- var hasValue = (value) => {
82
- if (Array.isArray(value)) {
83
- return value.length > 0;
84
- }
85
- return typeof value === "string" ? value.trim().length > 0 : value !== void 0;
86
- };
87
- var getValueLength = (value) => {
88
- if (Array.isArray(value)) {
89
- return value.length;
90
- }
91
- if (typeof value === "string") {
92
- return value.length;
93
- }
94
- return 0;
95
- };
96
- var toPatternTarget = (value) => {
97
- if (typeof value === "string") {
98
- return value;
99
- }
100
- if (Array.isArray(value)) {
101
- return value.map((entry) => typeof entry === "string" ? entry : entry.name).join(",");
102
- }
103
- return "";
104
- };
105
- var validateFieldValue = (field, value, rules, values) => {
106
- const errors = [];
107
- const context = { field, value, values };
108
- const required = resolveRule(rules.required);
109
- const minLength = resolveRule(rules.minLength);
110
- const maxLength = resolveRule(rules.maxLength);
111
- const pattern = resolveRule(rules.pattern);
112
- if (required.value && !hasValue(value)) {
113
- errors.push(required.message ?? "This field is required.");
114
- }
115
- if (minLength.value !== void 0 && getValueLength(value) < minLength.value) {
116
- errors.push(minLength.message ?? `Minimum length is ${minLength.value}.`);
117
- }
118
- if (maxLength.value !== void 0 && getValueLength(value) > maxLength.value) {
119
- errors.push(maxLength.message ?? `Maximum length is ${maxLength.value}.`);
120
- }
121
- if (pattern.value && hasValue(value) && !pattern.value.test(toPatternTarget(value))) {
122
- errors.push(pattern.message ?? "Value does not match the required pattern.");
123
- }
124
- const customValidators = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
125
- for (const validator of customValidators) {
126
- const result = validator(context);
127
- if (typeof result === "string" && result.length > 0) {
128
- errors.push(result);
129
- }
130
- if (result === false) {
131
- errors.push("Validation failed.");
132
- }
427
+ var cloneErrors = (errors) => Object.fromEntries(
428
+ Object.entries(errors).map(([field, messages]) => [field, [...messages]])
429
+ );
430
+ var mergeErrors = (left, right) => {
431
+ const merged = { ...left };
432
+ for (const [field, messages] of Object.entries(right)) {
433
+ merged[field] = merged[field] ? [...merged[field], ...messages] : [...messages];
133
434
  }
134
- return errors;
435
+ return merged;
135
436
  };
136
- var validateValues = (values, schema) => {
137
- const errors = {};
138
- for (const [field, rules] of Object.entries(schema)) {
139
- const messages = validateFieldValue(field, values[field], rules, values);
140
- if (messages.length > 0) {
141
- errors[field] = messages;
142
- }
143
- }
437
+
438
+ // src/api/get-state.ts
439
+ var createGetState = (state2, helpers) => () => {
440
+ const values = helpers.getValues();
441
+ const errors = cloneErrors(helpers.getMergedErrors());
442
+ const filledFields = helpers.getTrackedFieldNames().filter((name) => {
443
+ const value = values[name];
444
+ if (Array.isArray(value)) return value.length > 0;
445
+ if (typeof value === "string") return value.trim().length > 0;
446
+ return value !== void 0;
447
+ });
144
448
  return {
449
+ ...createEmptyFormState(),
450
+ values,
451
+ errors,
452
+ filledFields,
145
453
  isValid: Object.keys(errors).length === 0,
146
- errors
454
+ isValidated: state2.isValidated,
455
+ autoSubmit: state2.autoSubmitEnabled,
456
+ submitCount: state2.submitCount
147
457
  };
148
458
  };
149
459
 
150
- // src/internal/dom.ts
460
+ // src/core/dom.ts
151
461
  var isFormFieldElement = (value) => value instanceof HTMLInputElement || value instanceof HTMLSelectElement || value instanceof HTMLTextAreaElement;
152
462
  var resolveFormElement = (target) => {
153
- if (!target) {
154
- return null;
155
- }
156
- if (target instanceof HTMLFormElement) {
157
- return target;
158
- }
463
+ if (!target) return null;
464
+ if (target instanceof HTMLFormElement) return target;
159
465
  if (typeof target === "string") {
160
- const element = document.getElementById(target);
161
- return element instanceof HTMLFormElement ? element : null;
466
+ const node = document.getElementById(target);
467
+ return node instanceof HTMLFormElement ? node : null;
162
468
  }
163
469
  if ("current" in target) {
164
470
  return target.current instanceof HTMLFormElement ? target.current : null;
@@ -169,14 +475,11 @@ var getNamedFields = (form2) => Array.from(form2.querySelectorAll("input[name],
169
475
  isFormFieldElement
170
476
  );
171
477
  var queryNamedFields = (form2, name) => getNamedFields(form2).filter((field) => field.name === name);
478
+ var isFieldElement = (value) => isFormFieldElement(value);
172
479
  var isFieldFilled = (field) => {
173
480
  if (field instanceof HTMLInputElement) {
174
- if (field.type === "checkbox" || field.type === "radio") {
175
- return field.checked;
176
- }
177
- if (field.type === "file") {
178
- return Boolean(field.files && field.files.length > 0);
179
- }
481
+ if (field.type === "checkbox" || field.type === "radio") return field.checked;
482
+ if (field.type === "file") return Boolean(field.files && field.files.length > 0);
180
483
  return field.value.trim() !== "";
181
484
  }
182
485
  return field.value !== "";
@@ -184,35 +487,35 @@ var isFieldFilled = (field) => {
184
487
  var applyBooleanAttribute = (field, attribute, enabled) => {
185
488
  if (enabled) {
186
489
  field.setAttribute(attribute, "");
187
- return;
490
+ } else {
491
+ field.removeAttribute(attribute);
188
492
  }
189
- field.removeAttribute(attribute);
190
493
  };
191
494
  var readFieldValue = (fields) => {
192
- if (fields.length === 0) {
193
- return void 0;
194
- }
195
- const firstField = fields[0];
196
- if (!firstField) {
197
- return void 0;
198
- }
199
- if (firstField instanceof HTMLSelectElement || firstField instanceof HTMLTextAreaElement) {
200
- return firstField.value;
201
- }
202
- if (firstField.type === "radio") {
203
- const checked = fields.find(
204
- (field) => field instanceof HTMLInputElement && field.checked
205
- );
206
- return checked?.value ?? "";
207
- }
208
- if (firstField.type === "checkbox") {
209
- const values = fields.filter((field) => field instanceof HTMLInputElement && field.checked).map((field) => field.value);
210
- return values.length > 1 ? values : values[0] ?? "";
211
- }
212
- if (firstField.type === "file") {
213
- return firstField.files ? Array.from(firstField.files) : [];
495
+ if (fields.length === 0) return void 0;
496
+ const first = fields[0];
497
+ if (!first) return void 0;
498
+ if (first instanceof HTMLSelectElement || first instanceof HTMLTextAreaElement) {
499
+ return first.value;
500
+ }
501
+ if (first instanceof HTMLInputElement) {
502
+ if (first.type === "radio") {
503
+ const checked = fields.find(
504
+ (f) => f instanceof HTMLInputElement && f.checked
505
+ );
506
+ return checked?.value ?? "";
507
+ }
508
+ if (first.type === "checkbox") {
509
+ const values = fields.filter(
510
+ (f) => f instanceof HTMLInputElement && f.checked
511
+ ).map((f) => f.value);
512
+ return values.length > 1 ? values : values[0] ?? "";
513
+ }
514
+ if (first.type === "file") {
515
+ return first.files ? Array.from(first.files) : [];
516
+ }
214
517
  }
215
- return firstField.value;
518
+ return first.value;
216
519
  };
217
520
  var writeFieldValue = (fields, value) => {
218
521
  const normalizedArray = Array.isArray(value) ? value.map((item) => String(item)) : null;
@@ -228,9 +531,7 @@ var writeFieldValue = (fields, value) => {
228
531
  continue;
229
532
  }
230
533
  if (field.type === "file") {
231
- if (Array.isArray(value) && value.length === 0) {
232
- field.value = "";
233
- }
534
+ if (Array.isArray(value) && value.length === 0) field.value = "";
234
535
  continue;
235
536
  }
236
537
  field.value = normalizedValue;
@@ -241,9 +542,7 @@ var writeFieldValue = (fields, value) => {
241
542
  };
242
543
  var clearAttributes = (form2, attributes) => {
243
544
  for (const attribute of attributes) {
244
- form2.querySelectorAll(`[${attribute}]`).forEach((element) => {
245
- element.removeAttribute(attribute);
246
- });
545
+ form2.querySelectorAll(`[${attribute}]`).forEach((node) => node.removeAttribute(attribute));
247
546
  }
248
547
  };
249
548
  var submitForm = (form2) => {
@@ -261,25 +560,205 @@ var submitForm = (form2) => {
261
560
  form2.submit();
262
561
  };
263
562
 
264
- // src/internal/controller.ts
563
+ // src/api/get-value.ts
564
+ var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
565
+
566
+ // src/api/observe.ts
567
+ var createObserve = (state2, helpers) => (field, callback) => {
568
+ const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
569
+ callbacks.add(callback);
570
+ state2.watchedFields.set(field, callbacks);
571
+ if (state2.element) {
572
+ callback(state2.api.getValue(field), state2.api.getField(field), state2.element, state2.api.getState());
573
+ }
574
+ return () => {
575
+ const current = state2.watchedFields.get(field);
576
+ current?.delete(callback);
577
+ if (current && current.size === 0) {
578
+ state2.watchedFields.delete(field);
579
+ }
580
+ };
581
+ };
582
+
583
+ // src/api/on-submit.ts
584
+ var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
585
+ state2.submitHandlers.add({ callback, preventDefault });
586
+ return state2.api;
587
+ };
588
+
589
+ // src/api/prefill.ts
590
+ var createPrefill = (state2, _helpers) => (fieldName) => {
591
+ if (!state2.element) return state2.api;
592
+ const params = new URLSearchParams(window.location.search);
593
+ params.forEach((value, key) => {
594
+ if (fieldName && fieldName !== key) return;
595
+ state2.api.setValue(key, value);
596
+ });
597
+ return state2.api;
598
+ };
599
+
600
+ // src/api/reset.ts
601
+ var createReset = (state2, helpers) => () => {
602
+ if (!state2.element) return state2.api;
603
+ state2.element.reset();
604
+ state2.manualErrors = {};
605
+ state2.validationErrors = {};
606
+ clearAttributes(state2.element, [state2.attributes.error, state2.attributes.filled]);
607
+ if (state2.isValidated) helpers.syncVisualState();
608
+ helpers.notifySubscribers();
609
+ return state2.api;
610
+ };
611
+
612
+ // src/api/validate.ts
613
+ var createValidate = (state2, helpers) => (fields) => {
614
+ state2.isValidated = true;
615
+ helpers.syncVisualState(fields);
616
+ return helpers.validateNames(fields);
617
+ };
618
+
619
+ // src/api/revalidate.ts
620
+ var createRevalidate = createValidate;
621
+
622
+ // src/api/set-errors.ts
623
+ var createSetErrors = (state2, helpers) => (fields) => {
624
+ if (Array.isArray(fields)) {
625
+ state2.manualErrors = {
626
+ ...state2.manualErrors,
627
+ ...Object.fromEntries(fields.map((field) => [field, ["Invalid value."]]))
628
+ };
629
+ } else {
630
+ state2.manualErrors = {
631
+ ...state2.manualErrors,
632
+ ...cloneErrors(fields)
633
+ };
634
+ }
635
+ helpers.syncVisualState(Array.isArray(fields) ? fields : Object.keys(fields));
636
+ helpers.notifySubscribers();
637
+ return state2.api;
638
+ };
639
+
640
+ // src/api/set-value.ts
641
+ var createSetValue = (state2, helpers) => (name, value) => {
642
+ const fields = helpers.getFieldsByName(name);
643
+ if (fields.length === 0) return state2.api;
644
+ writeFieldValue(fields, value);
645
+ const firstField = fields[0];
646
+ if (!firstField) return state2.api;
647
+ const eventType = firstField instanceof HTMLSelectElement || firstField instanceof HTMLInputElement && (firstField.type === "checkbox" || firstField.type === "radio") ? "change" : "input";
648
+ firstField.dispatchEvent(new Event(eventType, { bubbles: true }));
649
+ return state2.api;
650
+ };
651
+
652
+ // src/api/subscribe.ts
653
+ var createSubscribe = (state2, _helpers) => (listener) => {
654
+ state2.subscribers.add(listener);
655
+ listener(state2.api.getState());
656
+ return () => {
657
+ state2.subscribers.delete(listener);
658
+ };
659
+ };
660
+
661
+ // src/api/unwatch.ts
662
+ var createUnwatch = (state2, _helpers) => (field, callback) => {
663
+ if (!field) {
664
+ state2.watchedFields.clear();
665
+ return state2.api;
666
+ }
667
+ if (!callback) {
668
+ state2.watchedFields.delete(field);
669
+ return state2.api;
670
+ }
671
+ const callbacks = state2.watchedFields.get(field);
672
+ callbacks?.delete(callback);
673
+ if (callbacks && callbacks.size === 0) {
674
+ state2.watchedFields.delete(field);
675
+ }
676
+ return state2.api;
677
+ };
678
+
679
+ // src/api/watch.ts
680
+ var createWatch = (state2, helpers) => (field, callback) => {
681
+ const observe = createObserve(state2, helpers);
682
+ observe(field, callback);
683
+ return state2.api;
684
+ };
685
+
686
+ // src/core/validation.ts
687
+ var resolveRule = (rule) => {
688
+ if (rule === void 0) {
689
+ return { value: void 0, message: void 0 };
690
+ }
691
+ if (typeof rule === "object" && rule !== null && "value" in rule) {
692
+ return { value: rule.value, message: rule.message };
693
+ }
694
+ return { value: rule, message: void 0 };
695
+ };
696
+ var hasValue = (value) => {
697
+ if (Array.isArray(value)) return value.length > 0;
698
+ if (typeof value === "string") return value.trim().length > 0;
699
+ return value !== void 0;
700
+ };
701
+ var getValueLength = (value) => {
702
+ if (Array.isArray(value)) return value.length;
703
+ if (typeof value === "string") return value.length;
704
+ return 0;
705
+ };
706
+ var toPatternTarget = (value) => {
707
+ if (typeof value === "string") return value;
708
+ if (Array.isArray(value)) {
709
+ return value.map((entry) => typeof entry === "string" ? entry : entry.name).join(",");
710
+ }
711
+ return "";
712
+ };
713
+ var validateFieldValue = (field, value, rules, values) => {
714
+ const errors = [];
715
+ const context = { field, value, values };
716
+ const required = resolveRule(rules.required);
717
+ const minLength = resolveRule(rules.minLength);
718
+ const maxLength = resolveRule(rules.maxLength);
719
+ const pattern = resolveRule(rules.pattern);
720
+ if (required.value && !hasValue(value)) {
721
+ errors.push(required.message ?? "This field is required.");
722
+ }
723
+ if (minLength.value !== void 0 && getValueLength(value) < minLength.value) {
724
+ errors.push(minLength.message ?? `Minimum length is ${minLength.value}.`);
725
+ }
726
+ if (maxLength.value !== void 0 && getValueLength(value) > maxLength.value) {
727
+ errors.push(maxLength.message ?? `Maximum length is ${maxLength.value}.`);
728
+ }
729
+ if (pattern.value && hasValue(value) && !pattern.value.test(toPatternTarget(value))) {
730
+ errors.push(pattern.message ?? "Value does not match the required pattern.");
731
+ }
732
+ const custom = rules.validate ? Array.isArray(rules.validate) ? rules.validate : [rules.validate] : [];
733
+ for (const validator of custom) {
734
+ const result = validator(context);
735
+ if (typeof result === "string" && result.length > 0) errors.push(result);
736
+ if (result === false) errors.push("Validation failed.");
737
+ }
738
+ return errors;
739
+ };
740
+ var validateValues = (values, schema) => {
741
+ const errors = {};
742
+ for (const [field, rules] of Object.entries(schema)) {
743
+ const messages = validateFieldValue(field, values[field], rules, values);
744
+ if (messages.length > 0) errors[field] = messages;
745
+ }
746
+ return {
747
+ isValid: Object.keys(errors).length === 0,
748
+ errors
749
+ };
750
+ };
751
+
752
+ // src/core/controller.ts
265
753
  var DEFAULT_ATTRIBUTES = {
266
754
  filled: "css-filled",
267
755
  error: "css-error"
268
756
  };
269
757
  var DEFAULT_OPTIONS = {
270
758
  autoValidate: true,
271
- clearErrorsOnSubmit: true
759
+ clearErrorsOnSubmit: true,
760
+ clearManualErrorsOnChange: true
272
761
  };
273
- var mergeErrors = (left, right) => {
274
- const merged = { ...left };
275
- for (const [field, messages] of Object.entries(right)) {
276
- merged[field] = merged[field] ? [...merged[field], ...messages] : [...messages];
277
- }
278
- return merged;
279
- };
280
- var cloneErrors = (errors) => Object.fromEntries(
281
- Object.entries(errors).map(([field, messages]) => [field, [...messages]])
282
- );
283
762
  var createFormController = (target, options = {}) => {
284
763
  const element = resolveFormElement(target);
285
764
  const attributes = { ...DEFAULT_ATTRIBUTES, ...options.attributes };
@@ -288,47 +767,49 @@ var createFormController = (target, options = {}) => {
288
767
  ...options,
289
768
  attributes
290
769
  };
291
- const validators = normalizedOptions.validators ?? {};
292
- const watchedFields = /* @__PURE__ */ new Map();
293
- const listeners = [];
294
- const subscribers = /* @__PURE__ */ new Set();
295
- const submitHandlers = /* @__PURE__ */ new Set();
296
- const fieldCache = /* @__PURE__ */ new Map();
297
- let manualErrors = {};
298
- let validationErrors = {};
299
- let isValidated = Boolean(normalizedOptions.autoValidate);
300
- let autoSubmitEnabled = false;
301
- let autoSubmitDebounce = 0;
302
- let submitCount = 0;
303
- let autoSubmitTimer = null;
304
- let mutationObserver = null;
305
- let isDestroyed = false;
306
- const notifySubscribers = () => {
307
- const state = api.getState();
308
- subscribers.forEach((listener) => listener(state));
770
+ const state2 = {
771
+ element,
772
+ options: normalizedOptions,
773
+ attributes,
774
+ validators: normalizedOptions.validators ?? {},
775
+ watchedFields: /* @__PURE__ */ new Map(),
776
+ subscribers: /* @__PURE__ */ new Set(),
777
+ submitHandlers: /* @__PURE__ */ new Set(),
778
+ fieldCache: /* @__PURE__ */ new Map(),
779
+ manualErrors: {},
780
+ validationErrors: {},
781
+ isValidated: Boolean(normalizedOptions.autoValidate),
782
+ autoSubmitEnabled: false,
783
+ autoSubmitDebounce: 0,
784
+ submitCount: 0,
785
+ autoSubmitTimer: null,
786
+ isDestroyed: false,
787
+ listeners: [],
788
+ mutationObserver: null,
789
+ formattedFields: /* @__PURE__ */ new Map(),
790
+ api: null
309
791
  };
310
- const addListener = (listenerTarget, type, handler) => {
311
- listenerTarget.addEventListener(type, handler);
312
- listeners.push({ element: listenerTarget, type, handler });
792
+ const notifySubscribers = () => {
793
+ if (!state2.api) return;
794
+ const snapshot = state2.api.getState();
795
+ state2.subscribers.forEach((listener) => listener(snapshot));
313
796
  };
314
- const clearFieldCache = () => {
315
- fieldCache.clear();
797
+ const addListener2 = (node, type, handler) => {
798
+ node.addEventListener(type, handler);
799
+ state2.listeners.push({ element: node, type, handler });
316
800
  };
801
+ const clearFieldCache = () => state2.fieldCache.clear();
317
802
  const getFieldsByName = (name) => {
318
- if (!element) {
319
- return [];
803
+ if (!state2.element) return [];
804
+ if (!state2.fieldCache.has(name)) {
805
+ state2.fieldCache.set(name, queryNamedFields(state2.element, name));
320
806
  }
321
- if (!fieldCache.has(name)) {
322
- fieldCache.set(name, queryNamedFields(element, name));
323
- }
324
- return fieldCache.get(name) ?? [];
807
+ return state2.fieldCache.get(name) ?? [];
325
808
  };
326
809
  const getTrackedFieldNames = () => {
327
- if (!element) {
328
- return Object.keys(validators);
329
- }
330
- const names = new Set(Object.keys(validators));
331
- for (const field of getNamedFields(element)) {
810
+ if (!state2.element) return Object.keys(state2.validators);
811
+ const names = new Set(Object.keys(state2.validators));
812
+ for (const field of getNamedFields(state2.element)) {
332
813
  names.add(field.name);
333
814
  }
334
815
  return Array.from(names);
@@ -336,15 +817,13 @@ var createFormController = (target, options = {}) => {
336
817
  const getValues = () => {
337
818
  const values = {};
338
819
  for (const name of getTrackedFieldNames()) {
339
- values[name] = api.getValue(name);
820
+ values[name] = readFieldValue(getFieldsByName(name));
340
821
  }
341
822
  return values;
342
823
  };
343
- const getMergedErrors = () => mergeErrors(validationErrors, manualErrors);
824
+ const getMergedErrors = () => mergeErrors(state2.validationErrors, state2.manualErrors);
344
825
  const syncVisualState = (names) => {
345
- if (!element) {
346
- return;
347
- }
826
+ if (!state2.element) return;
348
827
  const targetNames = names ?? getTrackedFieldNames();
349
828
  const errors = getMergedErrors();
350
829
  for (const name of targetNames) {
@@ -356,24 +835,23 @@ var createFormController = (target, options = {}) => {
356
835
  }
357
836
  }
358
837
  };
838
+ void readFieldValue;
839
+ void writeFieldValue;
359
840
  const validateNames = (names) => {
360
- const targetNames = names ?? Object.keys(validators);
841
+ const targetNames = names ?? Object.keys(state2.validators);
361
842
  const values = getValues();
362
- const nextValidationErrors = names ? cloneErrors(validationErrors) : {};
843
+ const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
363
844
  for (const name of targetNames) {
364
- const rules = validators[name];
845
+ const rules = state2.validators[name];
365
846
  if (!rules) {
366
847
  delete nextValidationErrors[name];
367
848
  continue;
368
849
  }
369
- const errors = validateFieldValue(name, values[name], rules, values);
370
- if (errors.length > 0) {
371
- nextValidationErrors[name] = errors;
372
- } else {
373
- delete nextValidationErrors[name];
374
- }
850
+ const messages = validateFieldValue(name, values[name], rules, values);
851
+ if (messages.length > 0) nextValidationErrors[name] = messages;
852
+ else delete nextValidationErrors[name];
375
853
  }
376
- validationErrors = nextValidationErrors;
854
+ state2.validationErrors = nextValidationErrors;
377
855
  syncVisualState(names);
378
856
  return {
379
857
  isValid: Object.keys(getMergedErrors()).length === 0,
@@ -381,42 +859,49 @@ var createFormController = (target, options = {}) => {
381
859
  };
382
860
  };
383
861
  const emitFieldWatchers = (name) => {
384
- if (!element) {
385
- return;
386
- }
387
- const callbacks = watchedFields.get(name);
388
- if (!callbacks || callbacks.size === 0) {
389
- return;
390
- }
391
- const value = api.getValue(name);
392
- const state = api.getState();
393
- callbacks.forEach((callback) => callback(value, element, state));
862
+ if (!state2.element || !state2.api) return;
863
+ const callbacks = state2.watchedFields.get(name);
864
+ if (!callbacks || callbacks.size === 0) return;
865
+ const value = state2.api.getValue(name);
866
+ const fieldElement = state2.api.getField(name);
867
+ const snapshot = state2.api.getState();
868
+ callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
394
869
  };
395
870
  const scheduleAutoSubmit = () => {
396
- if (!element || !autoSubmitEnabled) {
397
- return;
398
- }
399
- if (autoSubmitTimer) {
400
- clearTimeout(autoSubmitTimer);
401
- }
402
- if (autoSubmitDebounce > 0) {
403
- autoSubmitTimer = setTimeout(() => submitForm(element), autoSubmitDebounce);
871
+ if (!state2.element || !state2.autoSubmitEnabled) return;
872
+ if (state2.autoSubmitTimer) clearTimeout(state2.autoSubmitTimer);
873
+ if (state2.autoSubmitDebounce > 0) {
874
+ state2.autoSubmitTimer = setTimeout(
875
+ () => submitForm(state2.element),
876
+ state2.autoSubmitDebounce
877
+ );
404
878
  return;
405
879
  }
406
- submitForm(element);
880
+ submitForm(state2.element);
881
+ };
882
+ const helpers = {
883
+ notifySubscribers,
884
+ clearFieldCache,
885
+ getFieldsByName,
886
+ getTrackedFieldNames,
887
+ getValues,
888
+ getMergedErrors,
889
+ syncVisualState,
890
+ validateNames,
891
+ emitFieldWatchers,
892
+ scheduleAutoSubmit
407
893
  };
408
894
  const handleDelegatedEvent = (event) => {
409
895
  const target2 = event.target;
410
- if (!(target2 instanceof Element) || !isFieldElement(target2)) {
411
- return;
412
- }
896
+ if (!(target2 instanceof Element) || !isFieldElement(target2)) return;
413
897
  const name = target2.name;
414
- if (!name) {
415
- return;
416
- }
898
+ if (!name) return;
417
899
  clearFieldCache();
900
+ if (normalizedOptions.clearManualErrorsOnChange) {
901
+ delete state2.manualErrors[name];
902
+ }
418
903
  syncVisualState([name]);
419
- if (isValidated && validators[name]) {
904
+ if (state2.isValidated && state2.validators[name]) {
420
905
  validateNames([name]);
421
906
  }
422
907
  emitFieldWatchers(name);
@@ -424,295 +909,97 @@ var createFormController = (target, options = {}) => {
424
909
  scheduleAutoSubmit();
425
910
  };
426
911
  const handleSubmitEvent = (event) => {
427
- if (!element || isDestroyed) {
428
- return;
429
- }
430
- if (normalizedOptions.clearErrorsOnSubmit) {
431
- manualErrors = {};
432
- }
433
- const validation = api.validate();
434
- const handlers = Array.from(submitHandlers);
912
+ if (!state2.element || state2.isDestroyed || !state2.api) return;
913
+ if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
914
+ const validation = state2.api.validate();
915
+ const handlers = Array.from(state2.submitHandlers);
435
916
  const shouldPrevent = validation.isValid ? handlers.some((handler) => handler.preventDefault) : true;
436
- if (shouldPrevent) {
437
- event.preventDefault();
438
- }
439
- submitCount += 1;
917
+ if (shouldPrevent) event.preventDefault();
918
+ state2.submitCount += 1;
440
919
  notifySubscribers();
441
- if (!validation.isValid) {
442
- return;
443
- }
444
- const { data, formData } = api.getData();
445
- const state = api.getState();
446
- handlers.forEach((handler) => handler.callback(element, data, formData, state));
920
+ if (!validation.isValid) return;
921
+ const { data, formData } = state2.api.getData();
922
+ const snapshot = state2.api.getState();
923
+ handlers.forEach(
924
+ (handler) => handler.callback(state2.element, data, formData, snapshot)
925
+ );
447
926
  };
448
927
  const startMutationObserver = () => {
449
- if (!element || typeof MutationObserver === "undefined") {
450
- return;
451
- }
452
- mutationObserver = new MutationObserver(() => {
928
+ if (!state2.element || typeof MutationObserver === "undefined") return;
929
+ state2.mutationObserver = new MutationObserver(() => {
453
930
  clearFieldCache();
454
- if (isValidated) {
931
+ if (state2.isValidated) {
455
932
  syncVisualState();
456
933
  validateNames();
457
934
  }
458
935
  notifySubscribers();
459
936
  });
460
- mutationObserver.observe(element, {
937
+ state2.mutationObserver.observe(state2.element, {
461
938
  childList: true,
462
939
  subtree: true,
463
940
  attributes: true,
464
941
  attributeFilter: ["name", "type"]
465
942
  });
466
943
  };
467
- const isFieldElement = (value) => value instanceof HTMLInputElement || value instanceof HTMLSelectElement || value instanceof HTMLTextAreaElement;
468
944
  const api = {
469
- element,
470
- f: element,
471
- options: normalizedOptions,
472
- onSubmit(callback, preventDefault = true) {
473
- submitHandlers.add({ callback, preventDefault });
474
- return api;
475
- },
476
- watch(field, callback) {
477
- api.observe(field, callback);
478
- return api;
479
- },
480
- observe(field, callback) {
481
- const callbacks = watchedFields.get(field) ?? /* @__PURE__ */ new Set();
482
- callbacks.add(callback);
483
- watchedFields.set(field, callbacks);
484
- if (element) {
485
- callback(api.getValue(field), element, api.getState());
486
- }
487
- return () => {
488
- const currentCallbacks = watchedFields.get(field);
489
- currentCallbacks?.delete(callback);
490
- if (currentCallbacks && currentCallbacks.size === 0) {
491
- watchedFields.delete(field);
492
- }
493
- };
494
- },
495
- unwatch(field, callback) {
496
- if (!field) {
497
- watchedFields.clear();
498
- return api;
499
- }
500
- if (!callback) {
501
- watchedFields.delete(field);
502
- return api;
503
- }
504
- const callbacks = watchedFields.get(field);
505
- callbacks?.delete(callback);
506
- if (callbacks && callbacks.size === 0) {
507
- watchedFields.delete(field);
508
- }
509
- return api;
510
- },
511
- subscribe(listener) {
512
- subscribers.add(listener);
513
- listener(api.getState());
514
- return () => {
515
- subscribers.delete(listener);
516
- };
517
- },
518
- prefill(fieldName) {
519
- if (!element) {
520
- return api;
521
- }
522
- const queryParams = new URLSearchParams(window.location.search);
523
- queryParams.forEach((value, key) => {
524
- if (fieldName && fieldName !== key) {
525
- return;
526
- }
527
- api.setValue(key, value);
528
- });
529
- return api;
530
- },
531
- append({ tag, content, class: className, atStart = false }) {
532
- if (!element) {
533
- return null;
534
- }
535
- clearFieldCache();
536
- if (className) {
537
- const classSelector = className.trim().split(/\s+/)[0];
538
- element.querySelector(`.${classSelector}`)?.remove();
539
- }
540
- const node = document.createElement(tag);
541
- if (className) {
542
- node.className = className;
543
- }
544
- node.innerHTML = content;
545
- if (atStart && element.firstChild) {
546
- element.insertBefore(node, element.firstChild);
547
- } else {
548
- element.appendChild(node);
549
- }
550
- return node;
551
- },
552
- setErrors(fields) {
553
- if (Array.isArray(fields)) {
554
- manualErrors = {
555
- ...manualErrors,
556
- ...Object.fromEntries(fields.map((field) => [field, ["Invalid value."]]))
557
- };
558
- } else {
559
- manualErrors = {
560
- ...manualErrors,
561
- ...cloneErrors(fields)
562
- };
563
- }
564
- syncVisualState(Array.isArray(fields) ? fields : Object.keys(fields));
565
- notifySubscribers();
566
- return api;
567
- },
568
- clearErrors(fields) {
569
- if (!fields) {
570
- manualErrors = {};
571
- syncVisualState();
572
- notifySubscribers();
573
- return api;
574
- }
575
- for (const field of fields) {
576
- delete manualErrors[field];
577
- }
578
- syncVisualState(fields);
579
- notifySubscribers();
580
- return api;
581
- },
582
- setValue(name, value) {
583
- const fields = getFieldsByName(name);
584
- if (fields.length === 0) {
585
- return api;
586
- }
587
- writeFieldValue(fields, value);
588
- const firstField = fields[0];
589
- if (!firstField) {
590
- return api;
591
- }
592
- const eventType = firstField instanceof HTMLSelectElement || firstField instanceof HTMLInputElement && (firstField.type === "checkbox" || firstField.type === "radio") ? "change" : "input";
593
- firstField.dispatchEvent(new Event(eventType, { bubbles: true }));
594
- return api;
595
- },
596
- validate(fields) {
597
- isValidated = true;
598
- syncVisualState(fields);
599
- return validateNames(fields);
600
- },
601
- revalidate(fields) {
602
- return api.validate(fields);
603
- },
604
- reset() {
605
- if (!element) {
606
- return api;
607
- }
608
- element.reset();
609
- manualErrors = {};
610
- validationErrors = {};
611
- clearAttributes(element, [attributes.error, attributes.filled]);
612
- if (isValidated) {
613
- syncVisualState();
614
- }
615
- notifySubscribers();
616
- return api;
617
- },
618
- autoSubmit(nextOptions = true) {
619
- autoSubmitEnabled = nextOptions !== false;
620
- autoSubmitDebounce = typeof nextOptions === "object" && nextOptions ? nextOptions.debounce ?? 0 : 0;
621
- notifySubscribers();
622
- return api;
945
+ get element() {
946
+ return state2.element;
623
947
  },
624
- disableAutoSubmit() {
625
- autoSubmitEnabled = false;
626
- if (autoSubmitTimer) {
627
- clearTimeout(autoSubmitTimer);
628
- autoSubmitTimer = null;
629
- }
630
- notifySubscribers();
631
- return api;
948
+ get f() {
949
+ return state2.element;
632
950
  },
633
- getValue(name) {
634
- return readFieldValue(getFieldsByName(name));
951
+ get options() {
952
+ return state2.options;
635
953
  },
636
- getField(name) {
637
- const fields = getFieldsByName(name);
638
- if (fields.length === 0) {
639
- return null;
640
- }
641
- const [firstField] = fields;
642
- if (!firstField) {
643
- return null;
644
- }
645
- return fields.length === 1 ? firstField : fields;
646
- },
647
- getData() {
648
- if (!element) {
649
- return { data: {}, formData: new FormData() };
650
- }
651
- return parseFormData(element);
652
- },
653
- getState() {
654
- const values = getValues();
655
- const errors = cloneErrors(getMergedErrors());
656
- const filledFields = getTrackedFieldNames().filter((name) => {
657
- const value = values[name];
658
- return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== void 0;
659
- });
660
- return {
661
- ...createEmptyFormState(),
662
- values,
663
- errors,
664
- filledFields,
665
- isValid: Object.keys(errors).length === 0,
666
- isValidated,
667
- autoSubmit: autoSubmitEnabled,
668
- submitCount
669
- };
670
- },
671
- destroy() {
672
- if (isDestroyed) {
673
- return;
674
- }
675
- isDestroyed = true;
676
- for (const listener of listeners) {
677
- listener.element.removeEventListener(listener.type, listener.handler);
678
- }
679
- listeners.length = 0;
680
- if (autoSubmitTimer) {
681
- clearTimeout(autoSubmitTimer);
682
- autoSubmitTimer = null;
683
- }
684
- mutationObserver?.disconnect();
685
- mutationObserver = null;
686
- watchedFields.clear();
687
- subscribers.clear();
688
- submitHandlers.clear();
689
- clearFieldCache();
690
- manualErrors = {};
691
- validationErrors = {};
692
- }
954
+ onSubmit: createOnSubmit(state2, helpers),
955
+ watch: createWatch(state2, helpers),
956
+ observe: createObserve(state2, helpers),
957
+ unwatch: createUnwatch(state2, helpers),
958
+ subscribe: createSubscribe(state2, helpers),
959
+ prefill: createPrefill(state2, helpers),
960
+ append: createAppend(state2, helpers),
961
+ setErrors: createSetErrors(state2, helpers),
962
+ clearErrors: createClearErrors(state2, helpers),
963
+ setValue: createSetValue(state2, helpers),
964
+ getValue: createGetValue(state2, helpers),
965
+ getField: createGetField(state2, helpers),
966
+ validate: createValidate(state2, helpers),
967
+ revalidate: createRevalidate(state2, helpers),
968
+ reset: createReset(state2, helpers),
969
+ autoSubmit: createAutoSubmit(state2, helpers),
970
+ disableAutoSubmit: createDisableAutoSubmit(state2, helpers),
971
+ getData: createGetData(state2, helpers),
972
+ getState: createGetState(state2, helpers),
973
+ format: createFormat(state2, helpers),
974
+ formatAll: createFormatAll(state2, helpers),
975
+ destroy: createDestroy(state2, helpers)
693
976
  };
694
- if (element) {
695
- addListener(element, "input", handleDelegatedEvent);
696
- addListener(element, "change", handleDelegatedEvent);
697
- addListener(element, "submit", handleSubmitEvent);
977
+ state2.api = api;
978
+ if (state2.element) {
979
+ addListener2(state2.element, "input", handleDelegatedEvent);
980
+ addListener2(state2.element, "change", handleDelegatedEvent);
981
+ addListener2(state2.element, "submit", handleSubmitEvent);
698
982
  startMutationObserver();
699
983
  }
700
984
  if (normalizedOptions.autoSubmit) {
701
- const autoSubmitOption = normalizedOptions.autoSubmit;
702
- api.autoSubmit(autoSubmitOption);
985
+ api.autoSubmit(normalizedOptions.autoSubmit);
986
+ }
987
+ if (normalizedOptions.formats) {
988
+ for (const config of Object.values(normalizedOptions.formats)) {
989
+ api.format(config);
990
+ }
703
991
  }
704
- if (isValidated) {
992
+ if (state2.isValidated) {
705
993
  syncVisualState();
706
994
  validateNames();
707
995
  }
708
996
  return api;
709
997
  };
710
998
 
711
- // src/vanilla/index.ts
712
- var form = (target, options) => createFormController(target, options);
999
+ // src/api/form.ts
1000
+ var form = (target, options = {}) => createFormController(target, options);
713
1001
  // Annotate the CommonJS export names for ESM import in node:
714
1002
  0 && (module.exports = {
715
- createEmptyFormState,
716
1003
  createFormController,
717
1004
  form,
718
1005
  parseFormData,