@coherent.js/forms 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1860 @@
1
+ // src/form-builder.js
2
+ var FormBuilder = class {
3
+ constructor(options = {}) {
4
+ this.options = {
5
+ validateOnChange: true,
6
+ validateOnBlur: true,
7
+ name: options.name || "form",
8
+ ...options
9
+ };
10
+ this.fields = /* @__PURE__ */ new Map();
11
+ this.groups = /* @__PURE__ */ new Map();
12
+ this.values = {};
13
+ this.errors = {};
14
+ this.touched = {};
15
+ this.initialValues = {};
16
+ this.submitHandler = null;
17
+ this.errorHandler = null;
18
+ this._isSubmitting = false;
19
+ }
20
+ /**
21
+ * Add a field to the form (alias for field)
22
+ */
23
+ addField(name, config = {}) {
24
+ return this.field(name, config);
25
+ }
26
+ /**
27
+ * Add a field to the form
28
+ */
29
+ field(name, config = {}) {
30
+ const fieldConfig = {
31
+ name,
32
+ type: config.type || "text",
33
+ label: config.label || name,
34
+ placeholder: config.placeholder || "",
35
+ defaultValue: config.defaultValue || "",
36
+ validators: config.validators || [],
37
+ required: config.required || false,
38
+ visible: config.visible !== false,
39
+ showWhen: config.showWhen,
40
+ ...config
41
+ };
42
+ this.fields.set(name, fieldConfig);
43
+ if (config.defaultValue !== void 0) {
44
+ this.values[name] = config.defaultValue;
45
+ this.initialValues[name] = config.defaultValue;
46
+ }
47
+ return this;
48
+ }
49
+ /**
50
+ * Remove a field from the form
51
+ */
52
+ removeField(name) {
53
+ this.fields.delete(name);
54
+ delete this.values[name];
55
+ delete this.errors[name];
56
+ delete this.touched[name];
57
+ return this;
58
+ }
59
+ /**
60
+ * Update field configuration
61
+ */
62
+ updateField(name, config) {
63
+ const field = this.fields.get(name);
64
+ if (field) {
65
+ this.fields.set(name, { ...field, ...config });
66
+ }
67
+ return this;
68
+ }
69
+ /**
70
+ * Get all fields as array
71
+ */
72
+ getFields() {
73
+ return Array.from(this.fields.values());
74
+ }
75
+ /**
76
+ * Add a field group
77
+ */
78
+ addGroup(name, config = {}) {
79
+ this.groups.set(name, {
80
+ name,
81
+ label: config.label || name,
82
+ fields: config.fields || [],
83
+ ...config
84
+ });
85
+ if (config.fields) {
86
+ config.fields.forEach((fieldConfig) => {
87
+ this.addField(fieldConfig.name, fieldConfig);
88
+ });
89
+ }
90
+ return this;
91
+ }
92
+ /**
93
+ * Get field configuration
94
+ */
95
+ getField(name) {
96
+ return this.fields.get(name);
97
+ }
98
+ /**
99
+ * Set field value
100
+ */
101
+ setValue(name, value) {
102
+ this.values[name] = value;
103
+ this.touched[name] = true;
104
+ const field = this.fields.get(name);
105
+ if (field && (field.validateOnChange || this.options.validateOnChange)) {
106
+ this.validateField(name);
107
+ }
108
+ }
109
+ /**
110
+ * Set multiple values
111
+ */
112
+ setValues(values) {
113
+ Object.assign(this.values, values);
114
+ return this;
115
+ }
116
+ /**
117
+ * Get field value
118
+ */
119
+ getValue(name) {
120
+ return this.values[name];
121
+ }
122
+ /**
123
+ * Get all values
124
+ */
125
+ getValues() {
126
+ return { ...this.values };
127
+ }
128
+ /**
129
+ * Get field error
130
+ */
131
+ getFieldError(name) {
132
+ return this.errors[name];
133
+ }
134
+ /**
135
+ * Check if form has errors
136
+ */
137
+ hasErrors() {
138
+ return Object.keys(this.errors).length > 0;
139
+ }
140
+ /**
141
+ * Clear all errors
142
+ */
143
+ clearErrors() {
144
+ this.errors = {};
145
+ return this;
146
+ }
147
+ /**
148
+ * Check if form is dirty (values changed from initial)
149
+ */
150
+ isDirty() {
151
+ return Object.keys(this.values).some((key) => {
152
+ return this.values[key] !== this.initialValues[key];
153
+ });
154
+ }
155
+ /**
156
+ * Check if form is valid
157
+ */
158
+ isValid() {
159
+ const result = this.validate();
160
+ return Object.keys(result).length === 0;
161
+ }
162
+ /**
163
+ * Validate a field
164
+ */
165
+ validateField(name) {
166
+ const field = this.fields.get(name);
167
+ if (!field) return null;
168
+ const showCondition = field.showWhen || field.showIf;
169
+ if (showCondition && !showCondition(this.values)) {
170
+ delete this.errors[name];
171
+ return null;
172
+ }
173
+ const value = this.values[name];
174
+ if (field.required && (value === void 0 || value === null || value === "")) {
175
+ const error = "This field is required";
176
+ this.errors[name] = error;
177
+ return error;
178
+ }
179
+ if (!value && !field.required) {
180
+ delete this.errors[name];
181
+ return null;
182
+ }
183
+ if (value) {
184
+ if (field.type === "email") {
185
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
186
+ if (!emailRegex.test(value)) {
187
+ const error = "Please enter a valid email address";
188
+ this.errors[name] = error;
189
+ return error;
190
+ }
191
+ } else if (field.type === "url") {
192
+ try {
193
+ new URL(value);
194
+ } catch {
195
+ const error = "Please enter a valid URL";
196
+ this.errors[name] = error;
197
+ return error;
198
+ }
199
+ } else if (field.type === "number") {
200
+ if (isNaN(Number(value))) {
201
+ const error = "Please enter a valid number";
202
+ this.errors[name] = error;
203
+ return error;
204
+ }
205
+ }
206
+ }
207
+ if (field.validate) {
208
+ const error = field.validate(value, this.values);
209
+ if (error) {
210
+ this.errors[name] = error;
211
+ return error;
212
+ }
213
+ }
214
+ for (const validator of field.validators || []) {
215
+ const error = validator(value, this.values);
216
+ if (error) {
217
+ this.errors[name] = error;
218
+ return error;
219
+ }
220
+ }
221
+ delete this.errors[name];
222
+ return null;
223
+ }
224
+ /**
225
+ * Validate all fields
226
+ */
227
+ validate() {
228
+ const errors = {};
229
+ for (const [name, field] of this.fields) {
230
+ const showCondition = field.showWhen || field.showIf;
231
+ if (showCondition && !showCondition(this.values)) {
232
+ continue;
233
+ }
234
+ const error = this.validateField(name);
235
+ if (error) {
236
+ errors[name] = error;
237
+ }
238
+ }
239
+ this.errors = errors;
240
+ return errors;
241
+ }
242
+ /**
243
+ * Set submit handler
244
+ */
245
+ onSubmit(handler) {
246
+ this.submitHandler = handler;
247
+ return this;
248
+ }
249
+ /**
250
+ * Set error handler
251
+ */
252
+ onError(handler) {
253
+ this.errorHandler = handler;
254
+ return this;
255
+ }
256
+ /**
257
+ * Submit the form
258
+ */
259
+ async submit() {
260
+ const errors = this.validate();
261
+ if (Object.keys(errors).length > 0) {
262
+ return { success: false, errors };
263
+ }
264
+ if (!this.submitHandler) {
265
+ return { success: true, data: this.values };
266
+ }
267
+ this._isSubmitting = true;
268
+ try {
269
+ const result = await this.submitHandler(this.values);
270
+ this._isSubmitting = false;
271
+ return { success: true, data: result };
272
+ } catch (error) {
273
+ this._isSubmitting = false;
274
+ if (this.errorHandler) {
275
+ this.errorHandler(error);
276
+ }
277
+ return { success: false, error };
278
+ }
279
+ }
280
+ /**
281
+ * Serialize form data
282
+ */
283
+ serialize() {
284
+ return { ...this.values };
285
+ }
286
+ /**
287
+ * Convert form to HTML string
288
+ */
289
+ toHTML() {
290
+ const fields = this.getFields();
291
+ let html = `<form name="${this.options.name}">`;
292
+ fields.forEach((field) => {
293
+ html += `<div class="form-field">`;
294
+ html += `<label for="${field.name}">${field.label}</label>`;
295
+ html += `<input type="${field.type}" name="${field.name}" id="${field.name}">`;
296
+ html += `</div>`;
297
+ });
298
+ html += `</form>`;
299
+ return html;
300
+ }
301
+ /**
302
+ * Mark field as touched
303
+ */
304
+ touch(name) {
305
+ this.touched[name] = true;
306
+ }
307
+ /**
308
+ * Build input component with validation metadata for hydration
309
+ */
310
+ buildInput(name) {
311
+ const field = this.fields.get(name);
312
+ if (!field) return null;
313
+ const value = this.values[name] || "";
314
+ const error = this.errors[name];
315
+ const isTouched = this.touched[name];
316
+ const validatorNames = field.validators.map((v) => {
317
+ if (typeof v === "function") return v.name || "custom";
318
+ if (typeof v === "string") return v;
319
+ return null;
320
+ }).filter(Boolean).join(",");
321
+ const inputProps = {
322
+ type: field.type,
323
+ name: field.name,
324
+ id: field.name,
325
+ value,
326
+ placeholder: field.placeholder,
327
+ "aria-invalid": error ? "true" : "false",
328
+ "aria-describedby": error ? `${name}-error` : void 0,
329
+ className: error && isTouched ? "error" : ""
330
+ };
331
+ if (field.required) {
332
+ inputProps.required = true;
333
+ inputProps["data-required"] = "true";
334
+ }
335
+ if (validatorNames) {
336
+ inputProps["data-validators"] = validatorNames;
337
+ }
338
+ return {
339
+ input: inputProps
340
+ };
341
+ }
342
+ /**
343
+ * Build label component
344
+ */
345
+ buildLabel(name) {
346
+ const field = this.fields.get(name);
347
+ if (!field) return null;
348
+ return {
349
+ label: {
350
+ for: field.name,
351
+ text: field.label
352
+ }
353
+ };
354
+ }
355
+ /**
356
+ * Build error component
357
+ */
358
+ buildError(name) {
359
+ const error = this.errors[name];
360
+ const isTouched = this.touched[name];
361
+ if (!error || !isTouched) return null;
362
+ return {
363
+ div: {
364
+ id: `${name}-error`,
365
+ className: "error-message",
366
+ role: "alert",
367
+ text: error
368
+ }
369
+ };
370
+ }
371
+ /**
372
+ * Build complete field component
373
+ */
374
+ buildField(name) {
375
+ const field = this.fields.get(name);
376
+ if (!field) return null;
377
+ const children = [
378
+ this.buildLabel(name),
379
+ this.buildInput(name)
380
+ ];
381
+ const error = this.buildError(name);
382
+ if (error) {
383
+ children.push(error);
384
+ }
385
+ return {
386
+ div: {
387
+ className: "form-field",
388
+ "data-field": name,
389
+ children
390
+ }
391
+ };
392
+ }
393
+ /**
394
+ * Build entire form
395
+ */
396
+ buildForm(options = {}) {
397
+ const fields = [];
398
+ for (const [name] of this.fields) {
399
+ fields.push(this.buildField(name));
400
+ }
401
+ if (options.submitButton !== false) {
402
+ fields.push({
403
+ button: {
404
+ type: "submit",
405
+ text: options.submitText || "Submit",
406
+ className: "submit-button"
407
+ }
408
+ });
409
+ }
410
+ return {
411
+ form: {
412
+ onsubmit: "handleSubmit(event)",
413
+ novalidate: true,
414
+ children: fields
415
+ }
416
+ };
417
+ }
418
+ /**
419
+ * Check if form is currently submitting
420
+ */
421
+ isSubmitting() {
422
+ return this._isSubmitting;
423
+ }
424
+ /**
425
+ * Get a field group
426
+ */
427
+ getGroup(name) {
428
+ return this.groups.get(name);
429
+ }
430
+ /**
431
+ * Check if a field is visible
432
+ */
433
+ isFieldVisible(name) {
434
+ const field = this.fields.get(name);
435
+ if (!field) return false;
436
+ const showCondition = field.showWhen || field.showIf;
437
+ if (showCondition) {
438
+ return showCondition(this.values);
439
+ }
440
+ return field.visible !== false;
441
+ }
442
+ /**
443
+ * Reset form
444
+ */
445
+ reset() {
446
+ this.values = {};
447
+ for (const [name, field] of this.fields) {
448
+ if (field.defaultValue !== void 0) {
449
+ this.values[name] = field.defaultValue;
450
+ } else {
451
+ this.values[name] = "";
452
+ }
453
+ }
454
+ this.errors = {};
455
+ this.touched = {};
456
+ this._isSubmitting = false;
457
+ return this;
458
+ }
459
+ };
460
+ function createFormBuilder(options = {}) {
461
+ return new FormBuilder(options);
462
+ }
463
+ function buildForm(fields, options = {}) {
464
+ const builder = new FormBuilder(options);
465
+ for (const [name, config] of Object.entries(fields)) {
466
+ builder.field(name, config);
467
+ }
468
+ return builder;
469
+ }
470
+
471
+ // src/validators.js
472
+ var validators = {
473
+ required: (value, options = {}) => {
474
+ if (value === null || value === void 0 || value === "") {
475
+ return options.message || validators.required.message || "This field is required";
476
+ }
477
+ return null;
478
+ },
479
+ email: (value) => {
480
+ if (!value) return null;
481
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
482
+ if (!emailRegex.test(value)) {
483
+ return "Please enter a valid email address";
484
+ }
485
+ return null;
486
+ },
487
+ minLength: (value, options = {}) => {
488
+ if (!value) return null;
489
+ const min = options.min || 0;
490
+ if (value.length < min) {
491
+ return options.message || `Must be at least ${min} characters`;
492
+ }
493
+ return null;
494
+ },
495
+ maxLength: (value, options = {}) => {
496
+ if (!value) return null;
497
+ const max = options.max || Infinity;
498
+ if (value.length > max) {
499
+ return options.message || `Must be no more than ${max} characters`;
500
+ }
501
+ return null;
502
+ },
503
+ min: (value, options = {}) => {
504
+ if (value === null || value === void 0 || value === "") return null;
505
+ const num = Number(value);
506
+ const minValue = options.min || 0;
507
+ if (isNaN(num) || num < minValue) {
508
+ return options.message || `Must be at least ${minValue}`;
509
+ }
510
+ return null;
511
+ },
512
+ max: (value, options = {}) => {
513
+ if (value === null || value === void 0 || value === "") return null;
514
+ const num = Number(value);
515
+ const maxValue = options.max || Infinity;
516
+ if (isNaN(num) || num > maxValue) {
517
+ return options.message || `Must be no more than ${maxValue}`;
518
+ }
519
+ return null;
520
+ },
521
+ pattern: (value, options = {}) => {
522
+ if (!value) return null;
523
+ const regex = options.pattern || options.regex;
524
+ if (regex && !regex.test(value)) {
525
+ return options.message || "Invalid format";
526
+ }
527
+ return null;
528
+ },
529
+ url: (value) => {
530
+ if (!value) return null;
531
+ try {
532
+ new URL(value);
533
+ return null;
534
+ } catch {
535
+ return "Please enter a valid URL";
536
+ }
537
+ },
538
+ number: (value) => {
539
+ if (value === null || value === void 0 || value === "") return null;
540
+ if (isNaN(Number(value))) {
541
+ return "Must be a valid number";
542
+ }
543
+ return null;
544
+ },
545
+ integer: (value) => {
546
+ if (value === null || value === void 0 || value === "") return null;
547
+ const num = Number(value);
548
+ if (isNaN(num) || !Number.isInteger(num)) {
549
+ return "Must be a whole number";
550
+ }
551
+ return null;
552
+ },
553
+ phone: (value) => {
554
+ if (!value) return null;
555
+ const phoneRegex = /^[\d\s\-\+\(\)]+$/;
556
+ if (!phoneRegex.test(value) || value.replace(/\D/g, "").length < 10) {
557
+ return "Please enter a valid phone number";
558
+ }
559
+ return null;
560
+ },
561
+ date: (value) => {
562
+ if (!value) return null;
563
+ const date = new Date(value);
564
+ if (isNaN(date.getTime())) {
565
+ return "Please enter a valid date";
566
+ }
567
+ return null;
568
+ },
569
+ match: (value, options = {}, translator, allValues = {}) => {
570
+ if (!value) return null;
571
+ const fieldName = options.field || options.fieldName;
572
+ if (value !== allValues[fieldName]) {
573
+ return options.message || `Must match ${fieldName}`;
574
+ }
575
+ return null;
576
+ },
577
+ custom: (value, options = {}, translator, allValues) => {
578
+ const validatorFn = options.validator || options.fn;
579
+ if (!validatorFn) return null;
580
+ const isValid = validatorFn(value, allValues);
581
+ return isValid ? null : options.message || "Validation failed";
582
+ },
583
+ fileType: (value, options = {}) => {
584
+ if (!value) return null;
585
+ const allowedTypes = options.accept || options.types || [];
586
+ if (value.type !== void 0) {
587
+ const fileType = value.type;
588
+ const fileExt = value.name ? value.name.split(".").pop().toLowerCase() : "";
589
+ const isValid = allowedTypes.some((type) => {
590
+ if (type.startsWith(".")) {
591
+ return fileExt === type.slice(1).toLowerCase();
592
+ }
593
+ if (type.includes("/")) {
594
+ if (type.endsWith("/*")) {
595
+ return fileType.startsWith(type.replace("/*", "/"));
596
+ }
597
+ return fileType === type;
598
+ }
599
+ return fileExt === type.toLowerCase();
600
+ });
601
+ if (!isValid) {
602
+ return options.message || `File type must be one of: ${allowedTypes.join(", ")}`;
603
+ }
604
+ return null;
605
+ }
606
+ return null;
607
+ },
608
+ fileSize: (value, options = {}) => {
609
+ if (!value) return null;
610
+ const maxSize = options.maxSize || Infinity;
611
+ if (value.size !== void 0) {
612
+ if (value.size > maxSize) {
613
+ const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
614
+ return options.message || `File size must be less than ${maxSizeMB}MB`;
615
+ }
616
+ return null;
617
+ }
618
+ return null;
619
+ },
620
+ fileExtension: (value, options = {}) => {
621
+ if (!value) return null;
622
+ const allowedExtensions = options.extensions || [];
623
+ const fileName = value.name || value;
624
+ const ext = `.${fileName.split(".").pop().toLowerCase()}`;
625
+ const isValid = allowedExtensions.some((allowed) => {
626
+ return ext === allowed.toLowerCase();
627
+ });
628
+ if (!isValid) {
629
+ return options.message || `File extension must be one of: ${allowedExtensions.join(", ")}`;
630
+ }
631
+ return null;
632
+ },
633
+ alpha: (value) => {
634
+ if (!value) return null;
635
+ const alphaRegex = /^[a-zA-Z]+$/;
636
+ if (!alphaRegex.test(value)) {
637
+ return "Must contain only letters";
638
+ }
639
+ return null;
640
+ },
641
+ alphanumeric: (value) => {
642
+ if (!value) return null;
643
+ const alphanumericRegex = /^[a-zA-Z0-9]+$/;
644
+ if (!alphanumericRegex.test(value)) {
645
+ return "Must contain only letters and numbers";
646
+ }
647
+ return null;
648
+ },
649
+ uppercase: (value) => {
650
+ if (!value) return null;
651
+ if (value !== value.toUpperCase()) {
652
+ return "Must be uppercase";
653
+ }
654
+ return null;
655
+ },
656
+ // Get a registered validator
657
+ get: (name) => {
658
+ return validators[name];
659
+ },
660
+ // Compose multiple validators
661
+ compose: (validatorList) => {
662
+ return (value, options, translator, allValues) => {
663
+ for (const validator of validatorList) {
664
+ const error = typeof validator === "function" ? validator(value, options, translator, allValues) : null;
665
+ if (error) {
666
+ return error;
667
+ }
668
+ }
669
+ return null;
670
+ };
671
+ },
672
+ // Debounce async validator
673
+ debounce: (validator, delay = 300) => {
674
+ let timeoutId;
675
+ return (value) => {
676
+ return new Promise((resolve) => {
677
+ clearTimeout(timeoutId);
678
+ timeoutId = setTimeout(async () => {
679
+ const result = await validator(value);
680
+ resolve(result);
681
+ }, delay);
682
+ });
683
+ };
684
+ },
685
+ // Cancellable async validator
686
+ cancellable: (validator) => {
687
+ let abortController;
688
+ const wrapped = async (value) => {
689
+ if (abortController) {
690
+ abortController.abort();
691
+ }
692
+ abortController = typeof AbortController !== "undefined" ? new AbortController() : null;
693
+ try {
694
+ return await validator(value, abortController ? abortController.signal : null);
695
+ } catch (error) {
696
+ if (error.name === "AbortError") {
697
+ return null;
698
+ }
699
+ throw error;
700
+ }
701
+ };
702
+ wrapped.cancel = () => {
703
+ if (abortController) {
704
+ abortController.abort();
705
+ }
706
+ };
707
+ return wrapped;
708
+ },
709
+ // Conditional validator
710
+ when: (condition, validator) => {
711
+ return (value, options = {}, translator, allValues = {}) => {
712
+ const context = options.min !== void 0 || options.max !== void 0 ? allValues : options;
713
+ const shouldValidate = typeof condition === "function" ? condition(value, context) : condition;
714
+ if (!shouldValidate) {
715
+ return null;
716
+ }
717
+ return typeof validator === "function" ? validator(value, options, translator, allValues) : null;
718
+ };
719
+ },
720
+ // Validator chain builder
721
+ chain: (options = {}) => {
722
+ const validatorList = [];
723
+ const stopOnFirstError = options.stopOnFirstError !== false;
724
+ const chain = {
725
+ required: (opts) => {
726
+ validatorList.push((v, o, t, a) => validators.required(v, opts || o, t, a));
727
+ return chain;
728
+ },
729
+ email: (opts) => {
730
+ validatorList.push((v, o, t, a) => validators.email(v, opts || o, t, a));
731
+ return chain;
732
+ },
733
+ minLength: (opts) => {
734
+ validatorList.push((v, o, t, a) => validators.minLength(v, opts || o, t, a));
735
+ return chain;
736
+ },
737
+ maxLength: (opts) => {
738
+ validatorList.push((v, o, t, a) => validators.maxLength(v, opts || o, t, a));
739
+ return chain;
740
+ },
741
+ custom: (fn, message) => {
742
+ validatorList.push((v, o, t, a) => {
743
+ const result = fn(v, a);
744
+ return result === null || result === true || result === void 0 ? null : message || result;
745
+ });
746
+ return chain;
747
+ },
748
+ validate: (value, opts, translator, allValues) => {
749
+ if (stopOnFirstError) {
750
+ for (const validator of validatorList) {
751
+ const error = validator(value, opts, translator, allValues);
752
+ if (error) {
753
+ return error;
754
+ }
755
+ }
756
+ return null;
757
+ } else {
758
+ const errors = [];
759
+ for (const validator of validatorList) {
760
+ const error = validator(value, opts, translator, allValues);
761
+ if (error) {
762
+ errors.push(error);
763
+ }
764
+ }
765
+ return errors.length > 0 ? errors : null;
766
+ }
767
+ }
768
+ };
769
+ return chain;
770
+ }
771
+ };
772
+ function validateField(value, validatorList, formData = {}) {
773
+ for (const validator of validatorList) {
774
+ const error = validator(value, formData);
775
+ if (error) {
776
+ return error;
777
+ }
778
+ }
779
+ return null;
780
+ }
781
+ function validateForm(formData, fieldValidators) {
782
+ const errors = {};
783
+ for (const [fieldName, validatorList] of Object.entries(fieldValidators)) {
784
+ const value = formData[fieldName];
785
+ const error = validateField(value, validatorList, formData);
786
+ if (error) {
787
+ errors[fieldName] = error;
788
+ }
789
+ }
790
+ return Object.keys(errors).length > 0 ? errors : null;
791
+ }
792
+ function registerValidator(name, validatorFn) {
793
+ validators[name] = validatorFn;
794
+ }
795
+ function composeValidators(...validatorFns) {
796
+ return (value, options, translator, allValues) => {
797
+ for (const validator of validatorFns) {
798
+ const error = validator(value, options, translator, allValues);
799
+ if (error) {
800
+ return error;
801
+ }
802
+ }
803
+ return null;
804
+ };
805
+ }
806
+
807
+ // src/form-hydration.js
808
+ function hydrateForm(formSelector, options = {}) {
809
+ if (typeof document === "undefined") {
810
+ console.warn("hydrateForm can only run in browser environment");
811
+ return null;
812
+ }
813
+ const form = typeof formSelector === "string" ? document.querySelector(formSelector) : formSelector;
814
+ if (!form) {
815
+ console.warn(`Form not found: ${formSelector}`);
816
+ return null;
817
+ }
818
+ const opts = {
819
+ validateOnBlur: true,
820
+ validateOnChange: false,
821
+ validateOnSubmit: true,
822
+ showErrorsOnTouch: true,
823
+ debounce: 300,
824
+ ...options
825
+ };
826
+ const state = {
827
+ values: {},
828
+ errors: {},
829
+ touched: {},
830
+ isSubmitting: false,
831
+ fields: /* @__PURE__ */ new Map()
832
+ };
833
+ const debounceTimers = /* @__PURE__ */ new Map();
834
+ function parseValidators(validatorString) {
835
+ if (!validatorString) return [];
836
+ return validatorString.split(",").map((v) => {
837
+ const trimmed = v.trim();
838
+ const [name, ...params] = trimmed.split(":");
839
+ if (validators[name]) {
840
+ return params.length > 0 ? validators[name](...params.map((p) => isNaN(p) ? p : Number(p))) : validators[name];
841
+ }
842
+ return null;
843
+ }).filter(Boolean);
844
+ }
845
+ function discoverFields() {
846
+ const inputs = form.querySelectorAll("[name]");
847
+ inputs.forEach((input) => {
848
+ const name = input.getAttribute("name");
849
+ const field = {
850
+ name,
851
+ element: input,
852
+ type: input.getAttribute("type") || "text",
853
+ required: input.hasAttribute("required") || input.dataset.required === "true",
854
+ validators: parseValidators(input.dataset.validators),
855
+ errorElement: null
856
+ };
857
+ const errorId = `${name}-error`;
858
+ field.errorElement = document.getElementById(errorId) || createErrorElement(name, input);
859
+ state.fields.set(name, field);
860
+ state.values[name] = getFieldValue(input);
861
+ state.touched[name] = false;
862
+ state.errors[name] = null;
863
+ });
864
+ }
865
+ function createErrorElement(name, inputElement) {
866
+ const errorDiv = document.createElement("div");
867
+ errorDiv.id = `${name}-error`;
868
+ errorDiv.className = "error-message";
869
+ errorDiv.setAttribute("role", "alert");
870
+ errorDiv.style.display = "none";
871
+ const fieldWrapper = inputElement.closest(".form-field") || inputElement.parentElement;
872
+ fieldWrapper.appendChild(errorDiv);
873
+ return errorDiv;
874
+ }
875
+ function getFieldValue(input) {
876
+ if (input.type === "checkbox") {
877
+ return input.checked;
878
+ } else if (input.type === "radio") {
879
+ const checked = form.querySelector(`[name="${input.name}"]:checked`);
880
+ return checked ? checked.value : null;
881
+ } else {
882
+ return input.value;
883
+ }
884
+ }
885
+ function setFieldValue(name, value) {
886
+ const field = state.fields.get(name);
887
+ if (!field) return;
888
+ const { element } = field;
889
+ if (element.type === "checkbox") {
890
+ element.checked = Boolean(value);
891
+ } else if (element.type === "radio") {
892
+ const radio = form.querySelector(`[name="${name}"][value="${value}"]`);
893
+ if (radio) radio.checked = true;
894
+ } else {
895
+ element.value = value;
896
+ }
897
+ state.values[name] = value;
898
+ }
899
+ function validateField2(name) {
900
+ const field = state.fields.get(name);
901
+ if (!field) return true;
902
+ const value = state.values[name];
903
+ let error = null;
904
+ if (field.required && (value === null || value === void 0 || value === "")) {
905
+ error = "This field is required";
906
+ }
907
+ if (!error && field.validators.length > 0) {
908
+ for (const validator of field.validators) {
909
+ const result = validator.validate ? validator.validate(value, state.values) : validator(value, state.values);
910
+ if (result !== true && result !== void 0 && result !== null) {
911
+ error = validator.message || result || "Validation failed";
912
+ break;
913
+ }
914
+ }
915
+ }
916
+ state.errors[name] = error;
917
+ displayError(name, error);
918
+ return !error;
919
+ }
920
+ function displayError(name, error) {
921
+ const field = state.fields.get(name);
922
+ if (!field) return;
923
+ const { element, errorElement } = field;
924
+ if (error && state.touched[name] && opts.showErrorsOnTouch) {
925
+ errorElement.textContent = error;
926
+ errorElement.style.display = "block";
927
+ element.setAttribute("aria-invalid", "true");
928
+ element.classList.add("error");
929
+ } else {
930
+ errorElement.textContent = "";
931
+ errorElement.style.display = "none";
932
+ element.setAttribute("aria-invalid", "false");
933
+ element.classList.remove("error");
934
+ }
935
+ }
936
+ function validateForm2() {
937
+ let isValid = true;
938
+ for (const name of state.fields.keys()) {
939
+ const fieldValid = validateField2(name);
940
+ if (!fieldValid) isValid = false;
941
+ }
942
+ return isValid;
943
+ }
944
+ function handleChange(event2) {
945
+ const input = event2.target;
946
+ const name = input.getAttribute("name");
947
+ if (!state.fields.has(name)) return;
948
+ state.values[name] = getFieldValue(input);
949
+ if (opts.validateOnChange) {
950
+ if (debounceTimers.has(name)) {
951
+ clearTimeout(debounceTimers.get(name));
952
+ }
953
+ const timer = setTimeout(() => {
954
+ validateField2(name);
955
+ debounceTimers.delete(name);
956
+ }, opts.debounce);
957
+ debounceTimers.set(name, timer);
958
+ }
959
+ }
960
+ function handleBlur(event2) {
961
+ const input = event2.target;
962
+ const name = input.getAttribute("name");
963
+ if (!state.fields.has(name)) return;
964
+ state.touched[name] = true;
965
+ if (opts.validateOnBlur) {
966
+ validateField2(name);
967
+ }
968
+ }
969
+ function handleSubmit(event2) {
970
+ event2.preventDefault();
971
+ for (const name of state.fields.keys()) {
972
+ state.touched[name] = true;
973
+ }
974
+ const isValid = validateForm2();
975
+ if (!isValid) {
976
+ const firstErrorField = Array.from(state.fields.values()).find((field) => state.errors[field.name]);
977
+ if (firstErrorField) {
978
+ firstErrorField.element.focus();
979
+ }
980
+ if (options.onError) {
981
+ options.onError(state.errors);
982
+ }
983
+ return;
984
+ }
985
+ state.isSubmitting = true;
986
+ const submitData = { ...state.values };
987
+ if (options.onSubmit) {
988
+ const result = options.onSubmit(submitData, event2);
989
+ if (result === false) {
990
+ state.isSubmitting = false;
991
+ return;
992
+ }
993
+ if (result && typeof result.then === "function") {
994
+ result.then(() => {
995
+ state.isSubmitting = false;
996
+ if (options.onSuccess) {
997
+ options.onSuccess(submitData);
998
+ }
999
+ }).catch((error) => {
1000
+ state.isSubmitting = false;
1001
+ if (options.onError) {
1002
+ options.onError(error);
1003
+ }
1004
+ });
1005
+ return;
1006
+ }
1007
+ }
1008
+ if (!options.onSubmit) {
1009
+ form.submit();
1010
+ }
1011
+ state.isSubmitting = false;
1012
+ }
1013
+ function attachEventListeners() {
1014
+ state.fields.forEach((field) => {
1015
+ field.element.addEventListener("input", handleChange);
1016
+ field.element.addEventListener("blur", handleBlur);
1017
+ });
1018
+ form.addEventListener("submit", handleSubmit);
1019
+ }
1020
+ function detachEventListeners() {
1021
+ state.fields.forEach((field) => {
1022
+ field.element.removeEventListener("input", handleChange);
1023
+ field.element.removeEventListener("blur", handleBlur);
1024
+ });
1025
+ form.removeEventListener("submit", handleSubmit);
1026
+ debounceTimers.forEach((timer) => clearTimeout(timer));
1027
+ debounceTimers.clear();
1028
+ }
1029
+ function reset() {
1030
+ state.fields.forEach((field) => {
1031
+ setFieldValue(field.name, "");
1032
+ state.touched[field.name] = false;
1033
+ state.errors[field.name] = null;
1034
+ displayError(field.name, null);
1035
+ });
1036
+ state.isSubmitting = false;
1037
+ form.reset();
1038
+ }
1039
+ discoverFields();
1040
+ attachEventListeners();
1041
+ return {
1042
+ validateField: validateField2,
1043
+ validateForm: validateForm2,
1044
+ setFieldValue,
1045
+ getFieldValue: (name) => state.values[name],
1046
+ getError: (name) => state.errors[name],
1047
+ getErrors: () => ({ ...state.errors }),
1048
+ getValues: () => ({ ...state.values }),
1049
+ setTouched: (name, touched = true) => {
1050
+ state.touched[name] = touched;
1051
+ },
1052
+ reset,
1053
+ destroy: detachEventListeners,
1054
+ isValid: () => Object.values(state.errors).every((e) => !e),
1055
+ isSubmitting: () => state.isSubmitting,
1056
+ getState: () => ({
1057
+ values: { ...state.values },
1058
+ errors: { ...state.errors },
1059
+ touched: { ...state.touched },
1060
+ isSubmitting: state.isSubmitting
1061
+ })
1062
+ };
1063
+ }
1064
+
1065
+ // src/validation.js
1066
+ var validators2 = {
1067
+ required: (message = "This field is required") => (value) => {
1068
+ if (value === null || value === void 0 || value === "") {
1069
+ return message;
1070
+ }
1071
+ return null;
1072
+ },
1073
+ minLength: (min, message = `Minimum length is ${min}`) => (value) => {
1074
+ if (value && value.length < min) {
1075
+ return message;
1076
+ }
1077
+ return null;
1078
+ },
1079
+ maxLength: (max, message = `Maximum length is ${max}`) => (value) => {
1080
+ if (value && value.length > max) {
1081
+ return message;
1082
+ }
1083
+ return null;
1084
+ },
1085
+ min: (min, message = `Minimum value is ${min}`) => (value) => {
1086
+ if (value !== null && value !== void 0 && Number(value) < min) {
1087
+ return message;
1088
+ }
1089
+ return null;
1090
+ },
1091
+ max: (max, message = `Maximum value is ${max}`) => (value) => {
1092
+ if (value !== null && value !== void 0 && Number(value) > max) {
1093
+ return message;
1094
+ }
1095
+ return null;
1096
+ },
1097
+ email: (message = "Invalid email address") => (value) => {
1098
+ if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
1099
+ return message;
1100
+ }
1101
+ return null;
1102
+ },
1103
+ url: (message = "Invalid URL") => (value) => {
1104
+ if (value) {
1105
+ try {
1106
+ new URL(value);
1107
+ } catch {
1108
+ return message;
1109
+ }
1110
+ }
1111
+ return null;
1112
+ },
1113
+ pattern: (regex, message = "Invalid format") => (value) => {
1114
+ if (value && !regex.test(value)) {
1115
+ return message;
1116
+ }
1117
+ return null;
1118
+ },
1119
+ matches: (fieldName, message = "Fields do not match") => (value, formData) => {
1120
+ if (value !== formData[fieldName]) {
1121
+ return message;
1122
+ }
1123
+ return null;
1124
+ },
1125
+ oneOf: (options, message = "Invalid option") => (value) => {
1126
+ if (value && !options.includes(value)) {
1127
+ return message;
1128
+ }
1129
+ return null;
1130
+ },
1131
+ custom: (fn, message = "Validation failed") => (value, formData) => {
1132
+ if (!fn(value, formData)) {
1133
+ return message;
1134
+ }
1135
+ return null;
1136
+ }
1137
+ };
1138
+ var FormValidator = class {
1139
+ constructor(schema = {}) {
1140
+ this.schema = schema;
1141
+ this.errors = {};
1142
+ this.touched = {};
1143
+ }
1144
+ /**
1145
+ * Validate a single field
1146
+ */
1147
+ validateField(name, value, formData = {}) {
1148
+ const fieldValidators = this.schema[name];
1149
+ if (!fieldValidators) {
1150
+ return null;
1151
+ }
1152
+ const validatorArray = Array.isArray(fieldValidators) ? fieldValidators : [fieldValidators];
1153
+ for (const validator of validatorArray) {
1154
+ const error = validator(value, formData);
1155
+ if (error) {
1156
+ return error;
1157
+ }
1158
+ }
1159
+ return null;
1160
+ }
1161
+ /**
1162
+ * Validate entire form
1163
+ */
1164
+ validate(formData) {
1165
+ const errors = {};
1166
+ let isValid = true;
1167
+ for (const [name, value] of Object.entries(formData)) {
1168
+ const error = this.validateField(name, value, formData);
1169
+ if (error) {
1170
+ errors[name] = error;
1171
+ isValid = false;
1172
+ }
1173
+ }
1174
+ for (const name of Object.keys(this.schema)) {
1175
+ if (!(name in formData)) {
1176
+ const error = this.validateField(name, void 0, formData);
1177
+ if (error) {
1178
+ errors[name] = error;
1179
+ isValid = false;
1180
+ }
1181
+ }
1182
+ }
1183
+ this.errors = errors;
1184
+ return { isValid, errors };
1185
+ }
1186
+ /**
1187
+ * Mark field as touched
1188
+ */
1189
+ touch(name) {
1190
+ this.touched[name] = true;
1191
+ }
1192
+ /**
1193
+ * Check if field is touched
1194
+ */
1195
+ isTouched(name) {
1196
+ return this.touched[name] || false;
1197
+ }
1198
+ /**
1199
+ * Get error for field
1200
+ */
1201
+ getError(name) {
1202
+ return this.errors[name] || null;
1203
+ }
1204
+ /**
1205
+ * Check if field has error
1206
+ */
1207
+ hasError(name) {
1208
+ return !!this.errors[name];
1209
+ }
1210
+ /**
1211
+ * Clear errors
1212
+ */
1213
+ clearErrors() {
1214
+ this.errors = {};
1215
+ }
1216
+ /**
1217
+ * Clear touched state
1218
+ */
1219
+ clearTouched() {
1220
+ this.touched = {};
1221
+ }
1222
+ /**
1223
+ * Reset validator
1224
+ */
1225
+ reset() {
1226
+ this.clearErrors();
1227
+ this.clearTouched();
1228
+ }
1229
+ };
1230
+ function createValidator(schema) {
1231
+ return new FormValidator(schema);
1232
+ }
1233
+ function validate(formData, schema) {
1234
+ const validator = new FormValidator(schema);
1235
+ return validator.validate(formData);
1236
+ }
1237
+
1238
+ // src/forms.js
1239
+ function createForm(options = {}) {
1240
+ const opts = {
1241
+ fields: {},
1242
+ validation: {
1243
+ strategy: "blur",
1244
+ debounce: 300,
1245
+ async: true,
1246
+ stopOnFirstError: false,
1247
+ revalidateOn: ["change", "blur"],
1248
+ ...options.validation
1249
+ },
1250
+ errors: {
1251
+ format: "detailed",
1252
+ display: "inline",
1253
+ customFormatter: null,
1254
+ ...options.errors
1255
+ },
1256
+ submission: {
1257
+ preventDefault: true,
1258
+ validateBeforeSubmit: true,
1259
+ disableOnSubmit: true,
1260
+ resetOnSuccess: false,
1261
+ onSuccess: null,
1262
+ onError: null,
1263
+ ...options.submission
1264
+ },
1265
+ state: {
1266
+ trackDirty: true,
1267
+ trackTouched: true,
1268
+ trackVisited: true,
1269
+ initialValues: {},
1270
+ resetValues: null,
1271
+ ...options.state
1272
+ },
1273
+ middleware: options.middleware || [],
1274
+ ...options
1275
+ };
1276
+ const state = {
1277
+ values: { ...opts.state.initialValues },
1278
+ errors: {},
1279
+ touched: {},
1280
+ dirty: {},
1281
+ visited: {},
1282
+ isSubmitting: false,
1283
+ isValidating: false,
1284
+ submitCount: 0,
1285
+ asyncValidations: /* @__PURE__ */ new Map()
1286
+ };
1287
+ const fields = /* @__PURE__ */ new Map();
1288
+ const stats = {
1289
+ validations: 0,
1290
+ asyncValidations: 0,
1291
+ submissions: 0,
1292
+ successfulSubmissions: 0,
1293
+ failedSubmissions: 0,
1294
+ middlewareExecutions: 0
1295
+ };
1296
+ function registerField(name, config) {
1297
+ fields.set(name, {
1298
+ name,
1299
+ type: config.type || "text",
1300
+ validators: config.validators || [],
1301
+ transform: config.transform || {},
1302
+ validateWhen: config.validateWhen,
1303
+ required: config.required || false,
1304
+ defaultValue: config.defaultValue,
1305
+ ...config
1306
+ });
1307
+ if (!(name in state.values)) {
1308
+ state.values[name] = config.defaultValue !== void 0 ? config.defaultValue : "";
1309
+ }
1310
+ state.errors[name] = [];
1311
+ state.touched[name] = false;
1312
+ state.dirty[name] = false;
1313
+ state.visited[name] = false;
1314
+ }
1315
+ function getField(name) {
1316
+ return fields.get(name);
1317
+ }
1318
+ function setFieldValue(name, value, shouldValidate = true) {
1319
+ const field = fields.get(name);
1320
+ if (!field) {
1321
+ console.warn(`Field ${name} not registered`);
1322
+ return;
1323
+ }
1324
+ if (field.transform?.input) {
1325
+ value = field.transform.input(value);
1326
+ }
1327
+ state.values[name] = value;
1328
+ if (opts.state.trackDirty) {
1329
+ const initialValue = opts.state.initialValues[name];
1330
+ state.dirty[name] = value !== initialValue;
1331
+ }
1332
+ if (shouldValidate && opts.validation.revalidateOn.includes("change")) {
1333
+ validateField2(name);
1334
+ }
1335
+ }
1336
+ function getFieldValue(name) {
1337
+ return state.values[name];
1338
+ }
1339
+ function setFieldTouched(name, touched = true) {
1340
+ if (!opts.state.trackTouched) return;
1341
+ state.touched[name] = touched;
1342
+ if (touched && opts.validation.revalidateOn.includes("blur")) {
1343
+ validateField2(name);
1344
+ }
1345
+ }
1346
+ function setFieldVisited(name, visited = true) {
1347
+ if (!opts.state.trackVisited) return;
1348
+ state.visited[name] = visited;
1349
+ }
1350
+ async function validateField2(name) {
1351
+ const field = fields.get(name);
1352
+ if (!field) return { valid: true, errors: [] };
1353
+ stats.validations++;
1354
+ if (field.validateWhen && !field.validateWhen(state.values)) {
1355
+ state.errors[name] = [];
1356
+ return { valid: true, errors: [] };
1357
+ }
1358
+ const value = state.values[name];
1359
+ const errors = [];
1360
+ if (state.asyncValidations.has(name)) {
1361
+ clearTimeout(state.asyncValidations.get(name));
1362
+ }
1363
+ if (field.required && (value === "" || value === null || value === void 0)) {
1364
+ errors.push({
1365
+ field: name,
1366
+ type: "required",
1367
+ message: `${name} is required`
1368
+ });
1369
+ state.errors[name] = errors;
1370
+ return { valid: false, errors };
1371
+ }
1372
+ for (const validator of field.validators) {
1373
+ try {
1374
+ let result;
1375
+ if (validator.validate.constructor.name === "AsyncFunction" || opts.validation.async) {
1376
+ stats.asyncValidations++;
1377
+ if (validator.debounce || opts.validation.debounce) {
1378
+ await new Promise((resolve) => {
1379
+ const timeoutId = setTimeout(resolve, validator.debounce || opts.validation.debounce);
1380
+ state.asyncValidations.set(name, timeoutId);
1381
+ });
1382
+ }
1383
+ result = await validator.validate(value, state.values);
1384
+ } else {
1385
+ result = validator.validate(value, state.values);
1386
+ }
1387
+ if (result !== true && result !== void 0 && result !== null) {
1388
+ errors.push({
1389
+ field: name,
1390
+ type: validator.name || "custom",
1391
+ message: validator.message || result || "Validation failed"
1392
+ });
1393
+ if (opts.validation.stopOnFirstError) {
1394
+ break;
1395
+ }
1396
+ }
1397
+ } catch (error) {
1398
+ errors.push({
1399
+ field: name,
1400
+ type: "error",
1401
+ message: error.message || "Validation error"
1402
+ });
1403
+ if (opts.validation.stopOnFirstError) {
1404
+ break;
1405
+ }
1406
+ }
1407
+ }
1408
+ state.errors[name] = errors;
1409
+ return { valid: errors.length === 0, errors };
1410
+ }
1411
+ async function validateForm2() {
1412
+ state.isValidating = true;
1413
+ const validationPromises = Array.from(fields.keys()).map(
1414
+ (name) => validateField2(name)
1415
+ );
1416
+ const results = await Promise.all(validationPromises);
1417
+ state.isValidating = false;
1418
+ const allErrors = results.reduce((acc, result, index) => {
1419
+ const fieldName = Array.from(fields.keys())[index];
1420
+ if (result.errors.length > 0) {
1421
+ acc[fieldName] = result.errors;
1422
+ }
1423
+ return acc;
1424
+ }, {});
1425
+ const isValid2 = Object.keys(allErrors).length === 0;
1426
+ return {
1427
+ valid: isValid2,
1428
+ errors: allErrors
1429
+ };
1430
+ }
1431
+ async function executeMiddleware(action, data) {
1432
+ if (opts.middleware.length === 0) return data;
1433
+ stats.middlewareExecutions++;
1434
+ let result = data;
1435
+ for (const middleware of opts.middleware) {
1436
+ try {
1437
+ const next = () => result;
1438
+ result = await middleware(action, result, next, state);
1439
+ } catch (error) {
1440
+ console.error("Middleware error:", error);
1441
+ throw error;
1442
+ }
1443
+ }
1444
+ return result;
1445
+ }
1446
+ function applyTransformations(values) {
1447
+ const transformed = { ...values };
1448
+ fields.forEach((field, name) => {
1449
+ if (field.transform?.output && name in transformed) {
1450
+ transformed[name] = field.transform.output(transformed[name]);
1451
+ }
1452
+ });
1453
+ return transformed;
1454
+ }
1455
+ async function handleSubmit(onSubmit) {
1456
+ stats.submissions++;
1457
+ try {
1458
+ if (opts.submission.preventDefault && typeof event !== "undefined") {
1459
+ event.preventDefault();
1460
+ }
1461
+ if (opts.submission.disableOnSubmit) {
1462
+ state.isSubmitting = true;
1463
+ }
1464
+ let values = { ...state.values };
1465
+ values = await executeMiddleware("beforeSubmit", values);
1466
+ if (opts.submission.validateBeforeSubmit) {
1467
+ const validation = await validateForm2();
1468
+ if (!validation.valid) {
1469
+ if (opts.submission.onError) {
1470
+ opts.submission.onError(validation.errors);
1471
+ }
1472
+ stats.failedSubmissions++;
1473
+ return { success: false, errors: validation.errors };
1474
+ }
1475
+ }
1476
+ values = applyTransformations(values);
1477
+ values = await executeMiddleware("afterValidation", values);
1478
+ const result = await onSubmit(values);
1479
+ await executeMiddleware("afterSubmit", result);
1480
+ state.submitCount++;
1481
+ stats.successfulSubmissions++;
1482
+ if (opts.submission.resetOnSuccess) {
1483
+ reset();
1484
+ }
1485
+ if (opts.submission.onSuccess) {
1486
+ opts.submission.onSuccess(result);
1487
+ }
1488
+ return { success: true, data: result };
1489
+ } catch (error) {
1490
+ stats.failedSubmissions++;
1491
+ if (opts.submission.onError) {
1492
+ opts.submission.onError(error);
1493
+ }
1494
+ await executeMiddleware("onError", error);
1495
+ return { success: false, error };
1496
+ } finally {
1497
+ state.isSubmitting = false;
1498
+ }
1499
+ }
1500
+ function reset(values) {
1501
+ const resetValues = values || opts.state.resetValues || opts.state.initialValues;
1502
+ state.values = { ...resetValues };
1503
+ state.errors = {};
1504
+ state.touched = {};
1505
+ state.dirty = {};
1506
+ state.visited = {};
1507
+ state.submitCount = 0;
1508
+ fields.forEach((field, name) => {
1509
+ if (!(name in state.values)) {
1510
+ state.values[name] = field.defaultValue !== void 0 ? field.defaultValue : "";
1511
+ }
1512
+ state.errors[name] = [];
1513
+ state.touched[name] = false;
1514
+ state.dirty[name] = false;
1515
+ state.visited[name] = false;
1516
+ });
1517
+ }
1518
+ function getErrors(fieldName) {
1519
+ if (fieldName) {
1520
+ return state.errors[fieldName] || [];
1521
+ }
1522
+ if (opts.errors.customFormatter) {
1523
+ return opts.errors.customFormatter(state.errors);
1524
+ }
1525
+ if (opts.errors.format === "simple") {
1526
+ return Object.values(state.errors).flat().map((e) => e.message);
1527
+ }
1528
+ return state.errors;
1529
+ }
1530
+ function isValid() {
1531
+ return Object.values(state.errors).every((errors) => errors.length === 0);
1532
+ }
1533
+ function isDirty(fieldName) {
1534
+ if (fieldName) {
1535
+ return state.dirty[fieldName] || false;
1536
+ }
1537
+ return Object.values(state.dirty).some((dirty) => dirty);
1538
+ }
1539
+ function isTouched(fieldName) {
1540
+ if (fieldName) {
1541
+ return state.touched[fieldName] || false;
1542
+ }
1543
+ return Object.values(state.touched).some((touched) => touched);
1544
+ }
1545
+ function getValues() {
1546
+ return { ...state.values };
1547
+ }
1548
+ function setValues(values, shouldValidate = false) {
1549
+ Object.entries(values).forEach(([name, value]) => {
1550
+ setFieldValue(name, value, shouldValidate);
1551
+ });
1552
+ }
1553
+ function getState() {
1554
+ return {
1555
+ values: { ...state.values },
1556
+ errors: { ...state.errors },
1557
+ touched: { ...state.touched },
1558
+ dirty: { ...state.dirty },
1559
+ visited: { ...state.visited },
1560
+ isSubmitting: state.isSubmitting,
1561
+ isValidating: state.isValidating,
1562
+ submitCount: state.submitCount,
1563
+ isValid: isValid(),
1564
+ isDirty: isDirty(),
1565
+ isTouched: isTouched()
1566
+ };
1567
+ }
1568
+ function getStats() {
1569
+ return {
1570
+ ...stats,
1571
+ fieldsRegistered: fields.size,
1572
+ activeAsyncValidations: state.asyncValidations.size
1573
+ };
1574
+ }
1575
+ Object.entries(opts.fields).forEach(([name, config]) => {
1576
+ registerField(name, config);
1577
+ });
1578
+ return {
1579
+ registerField,
1580
+ getField,
1581
+ setFieldValue,
1582
+ getFieldValue,
1583
+ setFieldTouched,
1584
+ setFieldVisited,
1585
+ validateField: validateField2,
1586
+ validateForm: validateForm2,
1587
+ handleSubmit,
1588
+ reset,
1589
+ getErrors,
1590
+ isValid,
1591
+ isDirty,
1592
+ isTouched,
1593
+ getValues,
1594
+ setValues,
1595
+ getState,
1596
+ getStats,
1597
+ // Expose state for testing
1598
+ _state: state
1599
+ };
1600
+ }
1601
+ var formValidators = {
1602
+ required: {
1603
+ name: "required",
1604
+ validate: (value) => {
1605
+ return value !== "" && value !== null && value !== void 0;
1606
+ },
1607
+ message: "This field is required"
1608
+ },
1609
+ email: {
1610
+ name: "email",
1611
+ validate: (value) => {
1612
+ if (!value) return true;
1613
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1614
+ return regex.test(value);
1615
+ },
1616
+ message: "Please enter a valid email address"
1617
+ },
1618
+ minLength: (min) => ({
1619
+ name: "minLength",
1620
+ validate: (value) => {
1621
+ if (!value) return true;
1622
+ return String(value).length >= min;
1623
+ },
1624
+ message: `Must be at least ${min} characters`
1625
+ }),
1626
+ maxLength: (max) => ({
1627
+ name: "maxLength",
1628
+ validate: (value) => {
1629
+ if (!value) return true;
1630
+ return String(value).length <= max;
1631
+ },
1632
+ message: `Must be no more than ${max} characters`
1633
+ }),
1634
+ pattern: (regex, message) => ({
1635
+ name: "pattern",
1636
+ validate: (value) => {
1637
+ if (!value) return true;
1638
+ return regex.test(value);
1639
+ },
1640
+ message: message || "Invalid format"
1641
+ }),
1642
+ asyncEmail: {
1643
+ name: "asyncEmail",
1644
+ validate: async (value) => {
1645
+ if (!value) return true;
1646
+ await new Promise((resolve) => setTimeout(resolve, 100));
1647
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1648
+ return regex.test(value);
1649
+ },
1650
+ message: "Please enter a valid email address",
1651
+ debounce: 500
1652
+ },
1653
+ asyncUnique: (checkFn) => ({
1654
+ name: "asyncUnique",
1655
+ validate: async (value) => {
1656
+ if (!value) return true;
1657
+ return await checkFn(value);
1658
+ },
1659
+ message: "This value is already taken",
1660
+ debounce: 500
1661
+ })
1662
+ };
1663
+ var enhancedForm = createForm();
1664
+
1665
+ // src/advanced-validation.js
1666
+ import { ReactiveState, observable, computed } from "@coherent.js/state/src/reactive-state.js";
1667
+ import { globalErrorHandler } from "@coherent.js/core/src/utils/_error-handler.js";
1668
+ var validationRules = {
1669
+ required: (value, params = true) => {
1670
+ if (!params) return true;
1671
+ const isEmpty = value === null || value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
1672
+ return !isEmpty || "This field is required";
1673
+ },
1674
+ min: (value, minValue) => {
1675
+ if (value === null || value === void 0 || value === "") return true;
1676
+ const num = Number(value);
1677
+ return isNaN(num) || num >= minValue || `Value must be at least ${minValue}`;
1678
+ },
1679
+ max: (value, maxValue) => {
1680
+ if (value === null || value === void 0 || value === "") return true;
1681
+ const num = Number(value);
1682
+ return isNaN(num) || num <= maxValue || `Value must be no more than ${maxValue}`;
1683
+ },
1684
+ minLength: (value, minLen) => {
1685
+ if (value === null || value === void 0) return true;
1686
+ const str = String(value);
1687
+ return str.length >= minLen || `Must be at least ${minLen} characters`;
1688
+ },
1689
+ maxLength: (value, maxLen) => {
1690
+ if (value === null || value === void 0) return true;
1691
+ const str = String(value);
1692
+ return str.length <= maxLen || `Must be no more than ${maxLen} characters`;
1693
+ },
1694
+ pattern: (value, regex, message = "Invalid format") => {
1695
+ if (value === null || value === void 0 || value === "") return true;
1696
+ const pattern = typeof regex === "string" ? new RegExp(regex) : regex;
1697
+ return pattern.test(String(value)) || message;
1698
+ },
1699
+ email: (value) => {
1700
+ if (value === null || value === void 0 || value === "") return true;
1701
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1702
+ return emailRegex.test(String(value)) || "Please enter a valid email address";
1703
+ },
1704
+ url: (value) => {
1705
+ if (value === null || value === void 0 || value === "") return true;
1706
+ try {
1707
+ new URL(String(value));
1708
+ return true;
1709
+ } catch {
1710
+ return "Please enter a valid URL";
1711
+ }
1712
+ },
1713
+ numeric: (value) => {
1714
+ if (value === null || value === void 0 || value === "") return true;
1715
+ return !isNaN(Number(value)) || "Must be a valid number";
1716
+ },
1717
+ integer: (value) => {
1718
+ if (value === null || value === void 0 || value === "") return true;
1719
+ const num = Number(value);
1720
+ return Number.isInteger(num) || "Must be a whole number";
1721
+ },
1722
+ alpha: (value) => {
1723
+ if (value === null || value === void 0 || value === "") return true;
1724
+ return /^[a-zA-Z]+$/.test(String(value)) || "Must contain only letters";
1725
+ },
1726
+ alphanumeric: (value) => {
1727
+ if (value === null || value === void 0 || value === "") return true;
1728
+ return /^[a-zA-Z0-9]+$/.test(String(value)) || "Must contain only letters and numbers";
1729
+ },
1730
+ equals: (value, otherValue, fieldName = "other field") => {
1731
+ return value === otherValue || `Must match ${fieldName}`;
1732
+ },
1733
+ oneOf: (value, options, message = "Invalid selection") => {
1734
+ if (value === null || value === void 0 || value === "") return true;
1735
+ return options.includes(value) || message;
1736
+ },
1737
+ custom: (value, validator, ...args) => {
1738
+ if (typeof validator !== "function") {
1739
+ throw new Error("Custom validator must be a function");
1740
+ }
1741
+ return validator(value, ...args);
1742
+ }
1743
+ };
1744
+ var binding = {
1745
+ /**
1746
+ * Two-way data binding for input elements
1747
+ */
1748
+ model(form, fieldName, options = {}) {
1749
+ return {
1750
+ value: form.getField(fieldName),
1751
+ oninput: (event2) => {
1752
+ const value = options.number ? Number(event2.target.value) : event2.target.value;
1753
+ form.setField(fieldName, value);
1754
+ },
1755
+ onblur: () => {
1756
+ form.handleBlur(fieldName);
1757
+ }
1758
+ };
1759
+ },
1760
+ /**
1761
+ * Checkbox binding
1762
+ */
1763
+ checkbox(form, fieldName) {
1764
+ return {
1765
+ checked: Boolean(form.getField(fieldName)),
1766
+ onchange: (event2) => {
1767
+ form.setField(fieldName, event2.target.checked);
1768
+ },
1769
+ onblur: () => {
1770
+ form.handleBlur(fieldName);
1771
+ }
1772
+ };
1773
+ },
1774
+ /**
1775
+ * Select dropdown binding
1776
+ */
1777
+ select(form, fieldName, options = {}) {
1778
+ return {
1779
+ value: form.getField(fieldName),
1780
+ onchange: (event2) => {
1781
+ const value = options.multiple ? Array.from(event2.target.selectedOptions, (opt) => opt.value) : event2.target.value;
1782
+ form.setField(fieldName, value);
1783
+ },
1784
+ onblur: () => {
1785
+ form.handleBlur(fieldName);
1786
+ }
1787
+ };
1788
+ },
1789
+ /**
1790
+ * Radio button binding
1791
+ */
1792
+ radio(form, fieldName, value) {
1793
+ return {
1794
+ checked: form.getField(fieldName) === value,
1795
+ value,
1796
+ onchange: (event2) => {
1797
+ if (event2.target.checked) {
1798
+ form.setField(fieldName, value);
1799
+ }
1800
+ }
1801
+ };
1802
+ }
1803
+ };
1804
+ var formComponents = {
1805
+ /**
1806
+ * Validation _error display
1807
+ */
1808
+ ValidationError({ form, field, className = "validation-_error" }) {
1809
+ const validator = form.getValidator(field);
1810
+ if (!validator || validator.errors.value.length === 0) {
1811
+ return null;
1812
+ }
1813
+ return {
1814
+ div: {
1815
+ className,
1816
+ children: validator.errors.value.map((_error) => ({
1817
+ span: { text: _error, className: "_error-message" }
1818
+ }))
1819
+ }
1820
+ };
1821
+ },
1822
+ /**
1823
+ * Form field wrapper with validation
1824
+ */
1825
+ FormField({ form, field, label, children, showErrors = true }) {
1826
+ const validator = form.getValidator(field);
1827
+ const state = validator ? validator.getState() : {};
1828
+ return {
1829
+ div: {
1830
+ className: `form-field ${state.hasError ? "has-_error" : ""} ${state.isTouched ? "touched" : ""}`,
1831
+ children: [
1832
+ label ? { label: { text: label, htmlFor: field } } : null,
1833
+ children,
1834
+ showErrors && state.hasError ? formComponents.ValidationError({ form, field }) : null
1835
+ ].filter(Boolean)
1836
+ }
1837
+ };
1838
+ }
1839
+ };
1840
+ export {
1841
+ FormBuilder,
1842
+ FormValidator,
1843
+ binding,
1844
+ buildForm,
1845
+ composeValidators,
1846
+ createForm,
1847
+ createFormBuilder,
1848
+ createValidator,
1849
+ enhancedForm,
1850
+ formComponents,
1851
+ formValidators,
1852
+ hydrateForm,
1853
+ registerValidator,
1854
+ validate,
1855
+ validateField,
1856
+ validateForm,
1857
+ validationRules,
1858
+ validators2 as validators
1859
+ };
1860
+ //# sourceMappingURL=index.js.map