@samline/forms 1.0.3 → 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 +87 -199
  2. package/dist/browser/global.d.ts +152 -0
  3. package/dist/browser/global.global.js +778 -474
  4. package/dist/browser/global.global.js.map +1 -1
  5. package/dist/index.cjs +753 -470
  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-PCKGH2WA.js +0 -11
  15. package/dist/chunk-PCKGH2WA.js.map +0 -1
  16. package/dist/chunk-WM2CL7IO.js +0 -576
  17. package/dist/chunk-WM2CL7IO.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 -734
  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 -764
  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-jYB7Wp7C.d.cts +0 -95
  37. package/dist/types-jYB7Wp7C.d.ts +0 -95
  38. package/dist/vanilla/index.cjs +0 -705
  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 -741
  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,26 +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
- clearManualErrorsOnChange: true,
272
- clearErrorsOnSubmit: true
759
+ clearErrorsOnSubmit: true,
760
+ clearManualErrorsOnChange: true
273
761
  };
274
- var mergeErrors = (left, right) => {
275
- const merged = { ...left };
276
- for (const [field, messages] of Object.entries(right)) {
277
- merged[field] = merged[field] ? [...merged[field], ...messages] : [...messages];
278
- }
279
- return merged;
280
- };
281
- var cloneErrors = (errors) => Object.fromEntries(
282
- Object.entries(errors).map(([field, messages]) => [field, [...messages]])
283
- );
284
762
  var createFormController = (target, options = {}) => {
285
763
  const element = resolveFormElement(target);
286
764
  const attributes = { ...DEFAULT_ATTRIBUTES, ...options.attributes };
@@ -289,47 +767,49 @@ var createFormController = (target, options = {}) => {
289
767
  ...options,
290
768
  attributes
291
769
  };
292
- const validators = normalizedOptions.validators ?? {};
293
- const watchedFields = /* @__PURE__ */ new Map();
294
- const listeners = [];
295
- const subscribers = /* @__PURE__ */ new Set();
296
- const submitHandlers = /* @__PURE__ */ new Set();
297
- const fieldCache = /* @__PURE__ */ new Map();
298
- let manualErrors = {};
299
- let validationErrors = {};
300
- let isValidated = Boolean(normalizedOptions.autoValidate);
301
- let autoSubmitEnabled = false;
302
- let autoSubmitDebounce = 0;
303
- let submitCount = 0;
304
- let autoSubmitTimer = null;
305
- let mutationObserver = null;
306
- let isDestroyed = false;
307
- const notifySubscribers = () => {
308
- const state = api.getState();
309
- 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
310
791
  };
311
- const addListener = (listenerTarget, type, handler) => {
312
- listenerTarget.addEventListener(type, handler);
313
- 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));
314
796
  };
315
- const clearFieldCache = () => {
316
- fieldCache.clear();
797
+ const addListener2 = (node, type, handler) => {
798
+ node.addEventListener(type, handler);
799
+ state2.listeners.push({ element: node, type, handler });
317
800
  };
801
+ const clearFieldCache = () => state2.fieldCache.clear();
318
802
  const getFieldsByName = (name) => {
319
- if (!element) {
320
- return [];
321
- }
322
- if (!fieldCache.has(name)) {
323
- fieldCache.set(name, queryNamedFields(element, name));
803
+ if (!state2.element) return [];
804
+ if (!state2.fieldCache.has(name)) {
805
+ state2.fieldCache.set(name, queryNamedFields(state2.element, name));
324
806
  }
325
- return fieldCache.get(name) ?? [];
807
+ return state2.fieldCache.get(name) ?? [];
326
808
  };
327
809
  const getTrackedFieldNames = () => {
328
- if (!element) {
329
- return Object.keys(validators);
330
- }
331
- const names = new Set(Object.keys(validators));
332
- 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)) {
333
813
  names.add(field.name);
334
814
  }
335
815
  return Array.from(names);
@@ -337,15 +817,13 @@ var createFormController = (target, options = {}) => {
337
817
  const getValues = () => {
338
818
  const values = {};
339
819
  for (const name of getTrackedFieldNames()) {
340
- values[name] = api.getValue(name);
820
+ values[name] = readFieldValue(getFieldsByName(name));
341
821
  }
342
822
  return values;
343
823
  };
344
- const getMergedErrors = () => mergeErrors(validationErrors, manualErrors);
824
+ const getMergedErrors = () => mergeErrors(state2.validationErrors, state2.manualErrors);
345
825
  const syncVisualState = (names) => {
346
- if (!element) {
347
- return;
348
- }
826
+ if (!state2.element) return;
349
827
  const targetNames = names ?? getTrackedFieldNames();
350
828
  const errors = getMergedErrors();
351
829
  for (const name of targetNames) {
@@ -357,24 +835,23 @@ var createFormController = (target, options = {}) => {
357
835
  }
358
836
  }
359
837
  };
838
+ void readFieldValue;
839
+ void writeFieldValue;
360
840
  const validateNames = (names) => {
361
- const targetNames = names ?? Object.keys(validators);
841
+ const targetNames = names ?? Object.keys(state2.validators);
362
842
  const values = getValues();
363
- const nextValidationErrors = names ? cloneErrors(validationErrors) : {};
843
+ const nextValidationErrors = names ? cloneErrors(state2.validationErrors) : {};
364
844
  for (const name of targetNames) {
365
- const rules = validators[name];
845
+ const rules = state2.validators[name];
366
846
  if (!rules) {
367
847
  delete nextValidationErrors[name];
368
848
  continue;
369
849
  }
370
- const errors = validateFieldValue(name, values[name], rules, values);
371
- if (errors.length > 0) {
372
- nextValidationErrors[name] = errors;
373
- } else {
374
- delete nextValidationErrors[name];
375
- }
850
+ const messages = validateFieldValue(name, values[name], rules, values);
851
+ if (messages.length > 0) nextValidationErrors[name] = messages;
852
+ else delete nextValidationErrors[name];
376
853
  }
377
- validationErrors = nextValidationErrors;
854
+ state2.validationErrors = nextValidationErrors;
378
855
  syncVisualState(names);
379
856
  return {
380
857
  isValid: Object.keys(getMergedErrors()).length === 0,
@@ -382,45 +859,49 @@ var createFormController = (target, options = {}) => {
382
859
  };
383
860
  };
384
861
  const emitFieldWatchers = (name) => {
385
- if (!element) {
386
- return;
387
- }
388
- const callbacks = watchedFields.get(name);
389
- if (!callbacks || callbacks.size === 0) {
390
- return;
391
- }
392
- const value = api.getValue(name);
393
- const state = api.getState();
394
- 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));
395
869
  };
396
870
  const scheduleAutoSubmit = () => {
397
- if (!element || !autoSubmitEnabled) {
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
+ );
398
878
  return;
399
879
  }
400
- if (autoSubmitTimer) {
401
- clearTimeout(autoSubmitTimer);
402
- }
403
- if (autoSubmitDebounce > 0) {
404
- autoSubmitTimer = setTimeout(() => submitForm(element), autoSubmitDebounce);
405
- return;
406
- }
407
- 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
408
893
  };
409
894
  const handleDelegatedEvent = (event) => {
410
895
  const target2 = event.target;
411
- if (!(target2 instanceof Element) || !isFieldElement(target2)) {
412
- return;
413
- }
896
+ if (!(target2 instanceof Element) || !isFieldElement(target2)) return;
414
897
  const name = target2.name;
415
- if (!name) {
416
- return;
417
- }
898
+ if (!name) return;
418
899
  clearFieldCache();
419
900
  if (normalizedOptions.clearManualErrorsOnChange) {
420
- delete manualErrors[name];
901
+ delete state2.manualErrors[name];
421
902
  }
422
903
  syncVisualState([name]);
423
- if (isValidated && validators[name]) {
904
+ if (state2.isValidated && state2.validators[name]) {
424
905
  validateNames([name]);
425
906
  }
426
907
  emitFieldWatchers(name);
@@ -428,295 +909,97 @@ var createFormController = (target, options = {}) => {
428
909
  scheduleAutoSubmit();
429
910
  };
430
911
  const handleSubmitEvent = (event) => {
431
- if (!element || isDestroyed) {
432
- return;
433
- }
434
- if (normalizedOptions.clearErrorsOnSubmit) {
435
- manualErrors = {};
436
- }
437
- const validation = api.validate();
438
- 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);
439
916
  const shouldPrevent = validation.isValid ? handlers.some((handler) => handler.preventDefault) : true;
440
- if (shouldPrevent) {
441
- event.preventDefault();
442
- }
443
- submitCount += 1;
917
+ if (shouldPrevent) event.preventDefault();
918
+ state2.submitCount += 1;
444
919
  notifySubscribers();
445
- if (!validation.isValid) {
446
- return;
447
- }
448
- const { data, formData } = api.getData();
449
- const state = api.getState();
450
- 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
+ );
451
926
  };
452
927
  const startMutationObserver = () => {
453
- if (!element || typeof MutationObserver === "undefined") {
454
- return;
455
- }
456
- mutationObserver = new MutationObserver(() => {
928
+ if (!state2.element || typeof MutationObserver === "undefined") return;
929
+ state2.mutationObserver = new MutationObserver(() => {
457
930
  clearFieldCache();
458
- if (isValidated) {
931
+ if (state2.isValidated) {
459
932
  syncVisualState();
460
933
  validateNames();
461
934
  }
462
935
  notifySubscribers();
463
936
  });
464
- mutationObserver.observe(element, {
937
+ state2.mutationObserver.observe(state2.element, {
465
938
  childList: true,
466
939
  subtree: true,
467
940
  attributes: true,
468
941
  attributeFilter: ["name", "type"]
469
942
  });
470
943
  };
471
- const isFieldElement = (value) => value instanceof HTMLInputElement || value instanceof HTMLSelectElement || value instanceof HTMLTextAreaElement;
472
944
  const api = {
473
- element,
474
- f: element,
475
- options: normalizedOptions,
476
- onSubmit(callback, preventDefault = true) {
477
- submitHandlers.add({ callback, preventDefault });
478
- return api;
479
- },
480
- watch(field, callback) {
481
- api.observe(field, callback);
482
- return api;
483
- },
484
- observe(field, callback) {
485
- const callbacks = watchedFields.get(field) ?? /* @__PURE__ */ new Set();
486
- callbacks.add(callback);
487
- watchedFields.set(field, callbacks);
488
- if (element) {
489
- callback(api.getValue(field), element, api.getState());
490
- }
491
- return () => {
492
- const currentCallbacks = watchedFields.get(field);
493
- currentCallbacks?.delete(callback);
494
- if (currentCallbacks && currentCallbacks.size === 0) {
495
- watchedFields.delete(field);
496
- }
497
- };
498
- },
499
- unwatch(field, callback) {
500
- if (!field) {
501
- watchedFields.clear();
502
- return api;
503
- }
504
- if (!callback) {
505
- watchedFields.delete(field);
506
- return api;
507
- }
508
- const callbacks = watchedFields.get(field);
509
- callbacks?.delete(callback);
510
- if (callbacks && callbacks.size === 0) {
511
- watchedFields.delete(field);
512
- }
513
- return api;
514
- },
515
- subscribe(listener) {
516
- subscribers.add(listener);
517
- listener(api.getState());
518
- return () => {
519
- subscribers.delete(listener);
520
- };
521
- },
522
- prefill(fieldName) {
523
- if (!element) {
524
- return api;
525
- }
526
- const queryParams = new URLSearchParams(window.location.search);
527
- queryParams.forEach((value, key) => {
528
- if (fieldName && fieldName !== key) {
529
- return;
530
- }
531
- api.setValue(key, value);
532
- });
533
- return api;
534
- },
535
- append({ tag, content, class: className, atStart = false }) {
536
- if (!element) {
537
- return null;
538
- }
539
- clearFieldCache();
540
- if (className) {
541
- const classSelector = className.trim().split(/\s+/)[0];
542
- element.querySelector(`.${classSelector}`)?.remove();
543
- }
544
- const node = document.createElement(tag);
545
- if (className) {
546
- node.className = className;
547
- }
548
- node.innerHTML = content;
549
- if (atStart && element.firstChild) {
550
- element.insertBefore(node, element.firstChild);
551
- } else {
552
- element.appendChild(node);
553
- }
554
- return node;
555
- },
556
- setErrors(fields) {
557
- if (Array.isArray(fields)) {
558
- manualErrors = {
559
- ...manualErrors,
560
- ...Object.fromEntries(fields.map((field) => [field, ["Invalid value."]]))
561
- };
562
- } else {
563
- manualErrors = {
564
- ...manualErrors,
565
- ...cloneErrors(fields)
566
- };
567
- }
568
- syncVisualState(Array.isArray(fields) ? fields : Object.keys(fields));
569
- notifySubscribers();
570
- return api;
571
- },
572
- clearErrors(fields) {
573
- if (!fields) {
574
- manualErrors = {};
575
- syncVisualState();
576
- notifySubscribers();
577
- return api;
578
- }
579
- for (const field of fields) {
580
- delete manualErrors[field];
581
- }
582
- syncVisualState(fields);
583
- notifySubscribers();
584
- return api;
585
- },
586
- setValue(name, value) {
587
- const fields = getFieldsByName(name);
588
- if (fields.length === 0) {
589
- return api;
590
- }
591
- writeFieldValue(fields, value);
592
- const firstField = fields[0];
593
- if (!firstField) {
594
- return api;
595
- }
596
- const eventType = firstField instanceof HTMLSelectElement || firstField instanceof HTMLInputElement && (firstField.type === "checkbox" || firstField.type === "radio") ? "change" : "input";
597
- firstField.dispatchEvent(new Event(eventType, { bubbles: true }));
598
- return api;
599
- },
600
- validate(fields) {
601
- isValidated = true;
602
- syncVisualState(fields);
603
- return validateNames(fields);
604
- },
605
- revalidate(fields) {
606
- return api.validate(fields);
607
- },
608
- reset() {
609
- if (!element) {
610
- return api;
611
- }
612
- element.reset();
613
- manualErrors = {};
614
- validationErrors = {};
615
- clearAttributes(element, [attributes.error, attributes.filled]);
616
- if (isValidated) {
617
- syncVisualState();
618
- }
619
- notifySubscribers();
620
- return api;
621
- },
622
- autoSubmit(nextOptions = true) {
623
- autoSubmitEnabled = nextOptions !== false;
624
- autoSubmitDebounce = typeof nextOptions === "object" && nextOptions ? nextOptions.debounce ?? 0 : 0;
625
- notifySubscribers();
626
- return api;
945
+ get element() {
946
+ return state2.element;
627
947
  },
628
- disableAutoSubmit() {
629
- autoSubmitEnabled = false;
630
- if (autoSubmitTimer) {
631
- clearTimeout(autoSubmitTimer);
632
- autoSubmitTimer = null;
633
- }
634
- notifySubscribers();
635
- return api;
948
+ get f() {
949
+ return state2.element;
636
950
  },
637
- getValue(name) {
638
- return readFieldValue(getFieldsByName(name));
951
+ get options() {
952
+ return state2.options;
639
953
  },
640
- getField(name) {
641
- const fields = getFieldsByName(name);
642
- if (fields.length === 0) {
643
- return null;
644
- }
645
- const [firstField] = fields;
646
- if (!firstField) {
647
- return null;
648
- }
649
- return fields.length === 1 ? firstField : fields;
650
- },
651
- getData() {
652
- if (!element) {
653
- return { data: {}, formData: new FormData() };
654
- }
655
- return parseFormData(element);
656
- },
657
- getState() {
658
- const values = getValues();
659
- const errors = cloneErrors(getMergedErrors());
660
- const filledFields = getTrackedFieldNames().filter((name) => {
661
- const value = values[name];
662
- return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== void 0;
663
- });
664
- return {
665
- ...createEmptyFormState(),
666
- values,
667
- errors,
668
- filledFields,
669
- isValid: Object.keys(errors).length === 0,
670
- isValidated,
671
- autoSubmit: autoSubmitEnabled,
672
- submitCount
673
- };
674
- },
675
- destroy() {
676
- if (isDestroyed) {
677
- return;
678
- }
679
- isDestroyed = true;
680
- for (const listener of listeners) {
681
- listener.element.removeEventListener(listener.type, listener.handler);
682
- }
683
- listeners.length = 0;
684
- if (autoSubmitTimer) {
685
- clearTimeout(autoSubmitTimer);
686
- autoSubmitTimer = null;
687
- }
688
- mutationObserver?.disconnect();
689
- mutationObserver = null;
690
- watchedFields.clear();
691
- subscribers.clear();
692
- submitHandlers.clear();
693
- clearFieldCache();
694
- manualErrors = {};
695
- validationErrors = {};
696
- }
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)
697
976
  };
698
- if (element) {
699
- addListener(element, "input", handleDelegatedEvent);
700
- addListener(element, "change", handleDelegatedEvent);
701
- 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);
702
982
  startMutationObserver();
703
983
  }
704
984
  if (normalizedOptions.autoSubmit) {
705
- const autoSubmitOption = normalizedOptions.autoSubmit;
706
- 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
+ }
707
991
  }
708
- if (isValidated) {
992
+ if (state2.isValidated) {
709
993
  syncVisualState();
710
994
  validateNames();
711
995
  }
712
996
  return api;
713
997
  };
714
998
 
715
- // src/vanilla/index.ts
716
- var form = (target, options) => createFormController(target, options);
999
+ // src/api/form.ts
1000
+ var form = (target, options = {}) => createFormController(target, options);
717
1001
  // Annotate the CommonJS export names for ESM import in node:
718
1002
  0 && (module.exports = {
719
- createEmptyFormState,
720
1003
  createFormController,
721
1004
  form,
722
1005
  parseFormData,