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