@coherent.js/forms 1.0.0 → 1.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.
package/dist/index.js CHANGED
@@ -1,4 +1,40 @@
1
1
  // src/form-builder.js
2
+ import { render as renderToHTML } from "@coherent.js/core";
3
+ var DEFAULT_CLASS_NAMES = {
4
+ /** Wrapper around label, control and error */
5
+ field: "form-field",
6
+ label: "",
7
+ /** Base class on the control, before any per-field className */
8
+ control: "",
9
+ /** Added to the control while it has a visible error */
10
+ invalid: "error",
11
+ /** The error message element */
12
+ error: "error-message",
13
+ submit: "submit-button"
14
+ };
15
+ var VALID_ATTRIBUTE_NAME = /^[A-Za-z_:][-A-Za-z0-9_:.]*$/;
16
+ var EVENT_HANDLER_NAME = /^on/i;
17
+ function safeAttributes(attributes) {
18
+ if (!attributes || typeof attributes !== "object") return {};
19
+ const safe = {};
20
+ for (const [name, value] of Object.entries(attributes)) {
21
+ if (!VALID_ATTRIBUTE_NAME.test(name)) {
22
+ console.warn(`[coherent.js/forms] Ignoring invalid attribute name: ${JSON.stringify(name)}`);
23
+ continue;
24
+ }
25
+ if (EVENT_HANDLER_NAME.test(name)) {
26
+ console.warn(
27
+ `[coherent.js/forms] Ignoring inline event handler "${name}". Attach handlers with hydrateForm instead.`
28
+ );
29
+ continue;
30
+ }
31
+ safe[name] = value;
32
+ }
33
+ return safe;
34
+ }
35
+ function joinClasses(...names) {
36
+ return names.filter(Boolean).join(" ");
37
+ }
2
38
  var FormBuilder = class {
3
39
  constructor(options = {}) {
4
40
  this.options = {
@@ -226,11 +262,8 @@ var FormBuilder = class {
226
262
  */
227
263
  validate() {
228
264
  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
- }
265
+ for (const [name] of this.fields) {
266
+ if (!this.isFieldVisible(name)) continue;
234
267
  const error = this.validateField(name);
235
268
  if (error) {
236
269
  errors[name] = error;
@@ -286,17 +319,8 @@ var FormBuilder = class {
286
319
  /**
287
320
  * Convert form to HTML string
288
321
  */
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;
322
+ toHTML(options = {}) {
323
+ return renderToHTML(this.buildForm(options));
300
324
  }
301
325
  /**
302
326
  * Mark field as touched
@@ -307,7 +331,7 @@ var FormBuilder = class {
307
331
  /**
308
332
  * Build input component with validation metadata for hydration
309
333
  */
310
- buildInput(name) {
334
+ buildInput(name, classNames = this.resolveClassNames()) {
311
335
  const field = this.fields.get(name);
312
336
  if (!field) return null;
313
337
  const value = this.values[name] || "";
@@ -318,16 +342,26 @@ var FormBuilder = class {
318
342
  if (typeof v === "string") return v;
319
343
  return null;
320
344
  }).filter(Boolean).join(",");
345
+ const controlClass = joinClasses(
346
+ classNames.control,
347
+ field.className,
348
+ error && isTouched ? classNames.invalid : null
349
+ );
321
350
  const inputProps = {
351
+ // Spread first: name, id, type and the aria-* pair below are the
352
+ // builder's own invariants, and hydration keys off them.
353
+ ...safeAttributes(field.attributes),
322
354
  type: field.type,
323
355
  name: field.name,
324
356
  id: field.name,
325
357
  value,
326
- placeholder: field.placeholder,
327
358
  "aria-invalid": error ? "true" : "false",
328
- "aria-describedby": error ? `${name}-error` : void 0,
329
- className: error && isTouched ? "error" : ""
359
+ "aria-describedby": error ? `${name}-error` : void 0
330
360
  };
361
+ if (field.placeholder) inputProps.placeholder = field.placeholder;
362
+ if (controlClass) inputProps.className = controlClass;
363
+ if (field.disabled) inputProps.disabled = true;
364
+ if (field.readonly) inputProps.readonly = true;
331
365
  if (field.required) {
332
366
  inputProps.required = true;
333
367
  inputProps["data-required"] = "true";
@@ -335,6 +369,29 @@ var FormBuilder = class {
335
369
  if (validatorNames) {
336
370
  inputProps["data-validators"] = validatorNames;
337
371
  }
372
+ if (field.type === "textarea" || field.type === "select") {
373
+ const { type: _type, value: _value, ...rest } = inputProps;
374
+ const props = { ...rest };
375
+ if (field.type === "textarea") {
376
+ return { textarea: { ...props, text: String(value) } };
377
+ }
378
+ const { placeholder: _placeholder, ...selectProps } = props;
379
+ return {
380
+ select: {
381
+ ...selectProps,
382
+ children: (field.options ?? []).map((option) => {
383
+ const { value: optionValue, label = optionValue } = typeof option === "object" && option !== null ? option : { value: option };
384
+ return {
385
+ option: {
386
+ value: optionValue,
387
+ selected: String(optionValue) === String(value) || void 0,
388
+ text: String(label)
389
+ }
390
+ };
391
+ })
392
+ }
393
+ };
394
+ }
338
395
  return {
339
396
  input: inputProps
340
397
  };
@@ -342,78 +399,103 @@ var FormBuilder = class {
342
399
  /**
343
400
  * Build label component
344
401
  */
345
- buildLabel(name) {
402
+ buildLabel(name, classNames = this.resolveClassNames()) {
346
403
  const field = this.fields.get(name);
347
404
  if (!field) return null;
348
- return {
349
- label: {
350
- for: field.name,
351
- text: field.label
352
- }
353
- };
405
+ const props = { for: field.name, text: field.label };
406
+ if (classNames.label) props.className = classNames.label;
407
+ return { label: props };
354
408
  }
355
409
  /**
356
410
  * Build error component
357
411
  */
358
- buildError(name) {
412
+ buildError(name, classNames = this.resolveClassNames()) {
359
413
  const error = this.errors[name];
360
414
  const isTouched = this.touched[name];
361
415
  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
- };
416
+ const props = { id: `${name}-error`, role: "alert", text: error };
417
+ if (classNames.error) props.className = classNames.error;
418
+ return { div: props };
419
+ }
420
+ /**
421
+ * Merge configured class names over the defaults
422
+ */
423
+ resolveClassNames(overrides = {}) {
424
+ return { ...DEFAULT_CLASS_NAMES, ...this.options.classNames, ...overrides };
370
425
  }
371
426
  /**
372
427
  * Build complete field component
373
428
  */
374
- buildField(name) {
429
+ buildField(name, classNames = this.resolveClassNames()) {
375
430
  const field = this.fields.get(name);
376
431
  if (!field) return null;
377
432
  const children = [
378
- this.buildLabel(name),
379
- this.buildInput(name)
433
+ this.buildLabel(name, classNames),
434
+ this.buildInput(name, classNames)
380
435
  ];
381
- const error = this.buildError(name);
436
+ const error = this.buildError(name, classNames);
382
437
  if (error) {
383
438
  children.push(error);
384
439
  }
385
- return {
386
- div: {
387
- className: "form-field",
388
- "data-field": name,
389
- children
390
- }
391
- };
440
+ const props = { "data-field": name, children };
441
+ if (classNames.field) props.className = classNames.field;
442
+ return { div: props };
392
443
  }
393
444
  /**
394
445
  * Build entire form
395
446
  */
396
447
  buildForm(options = {}) {
448
+ const settings = { ...this.options, ...options };
449
+ const classNames = this.resolveClassNames(options.classNames);
397
450
  const fields = [];
398
451
  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
- };
452
+ if (!this.isFieldVisible(name)) continue;
453
+ fields.push(this.buildField(name, classNames));
454
+ }
455
+ if (settings.submitButton !== false) {
456
+ const button = { type: "submit", text: settings.submitText || "Submit" };
457
+ if (classNames.submit) button.className = classNames.submit;
458
+ fields.push({ button });
459
+ }
460
+ const form = {};
461
+ if (settings.action) form.action = settings.action;
462
+ if (settings.method) form.method = settings.method;
463
+ if (settings.name) form.name = settings.name;
464
+ if (settings.id) form.id = settings.id;
465
+ if (settings.className) form.className = settings.className;
466
+ if (settings.enctype) form.enctype = settings.enctype;
467
+ if (settings.enhance) {
468
+ form.onsubmit = typeof settings.enhance === "string" ? settings.enhance : "handleSubmit(event)";
469
+ }
470
+ if (settings.novalidate === true) form.novalidate = true;
471
+ form.children = fields;
472
+ return { form };
473
+ }
474
+ /**
475
+ * Set the form action URL
476
+ */
477
+ setAction(action) {
478
+ this.options.action = action;
479
+ return this;
480
+ }
481
+ /**
482
+ * Set the form submission method
483
+ */
484
+ setMethod(method) {
485
+ this.options.method = method;
486
+ return this;
487
+ }
488
+ /**
489
+ * Build the form component (alias for buildForm)
490
+ */
491
+ build(options = {}) {
492
+ return this.buildForm(options);
493
+ }
494
+ /**
495
+ * Render the form to a component (alias for buildForm)
496
+ */
497
+ render(options = {}) {
498
+ return this.buildForm(options);
417
499
  }
418
500
  /**
419
501
  * Check if form is currently submitting
@@ -433,11 +515,12 @@ var FormBuilder = class {
433
515
  isFieldVisible(name) {
434
516
  const field = this.fields.get(name);
435
517
  if (!field) return false;
518
+ if (field.visible === false) return false;
436
519
  const showCondition = field.showWhen || field.showIf;
437
520
  if (showCondition) {
438
521
  return showCondition(this.values);
439
522
  }
440
- return field.visible !== false;
523
+ return true;
441
524
  }
442
525
  /**
443
526
  * Reset form
@@ -466,19 +549,199 @@ function createFormBuilder(options = {}) {
466
549
  }
467
550
  return form;
468
551
  }
469
- function buildForm(fields, options = {}) {
552
+ function buildForm(config = {}) {
553
+ const { fields = [], ...options } = Array.isArray(config) ? { fields: config } : config;
470
554
  const builder = new FormBuilder(options);
471
- for (const [name, config] of Object.entries(fields)) {
472
- builder.field(name, config);
555
+ if (Array.isArray(fields)) {
556
+ for (const field of fields) {
557
+ if (field && field.name) builder.field(field.name, field);
558
+ }
559
+ } else {
560
+ for (const [name, field] of Object.entries(fields)) {
561
+ builder.field(name, field);
562
+ }
473
563
  }
474
- return builder;
564
+ return builder.buildForm(options);
475
565
  }
476
566
 
477
- // src/validators.js
567
+ // src/validation.js
478
568
  var validators = {
569
+ required: (message = "This field is required") => (value) => {
570
+ if (value === null || value === void 0 || value === "") {
571
+ return message;
572
+ }
573
+ return null;
574
+ },
575
+ minLength: (min, message = `Minimum length is ${min}`) => (value) => {
576
+ if (value && value.length < min) {
577
+ return message;
578
+ }
579
+ return null;
580
+ },
581
+ maxLength: (max, message = `Maximum length is ${max}`) => (value) => {
582
+ if (value && value.length > max) {
583
+ return message;
584
+ }
585
+ return null;
586
+ },
587
+ min: (min, message = `Minimum value is ${min}`) => (value) => {
588
+ if (value !== null && value !== void 0 && Number(value) < min) {
589
+ return message;
590
+ }
591
+ return null;
592
+ },
593
+ max: (max, message = `Maximum value is ${max}`) => (value) => {
594
+ if (value !== null && value !== void 0 && Number(value) > max) {
595
+ return message;
596
+ }
597
+ return null;
598
+ },
599
+ email: (message = "Invalid email address") => (value) => {
600
+ if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
601
+ return message;
602
+ }
603
+ return null;
604
+ },
605
+ url: (message = "Invalid URL") => (value) => {
606
+ if (value) {
607
+ try {
608
+ new URL(value);
609
+ } catch {
610
+ return message;
611
+ }
612
+ }
613
+ return null;
614
+ },
615
+ pattern: (regex, message = "Invalid format") => (value) => {
616
+ if (value && !regex.test(value)) {
617
+ return message;
618
+ }
619
+ return null;
620
+ },
621
+ matches: (fieldName, message = "Fields do not match") => (value, formData) => {
622
+ if (value !== formData[fieldName]) {
623
+ return message;
624
+ }
625
+ return null;
626
+ },
627
+ oneOf: (options, message = "Invalid option") => (value) => {
628
+ if (value && !options.includes(value)) {
629
+ return message;
630
+ }
631
+ return null;
632
+ },
633
+ custom: (fn, message = "Validation failed") => (value, formData) => {
634
+ if (!fn(value, formData)) {
635
+ return message;
636
+ }
637
+ return null;
638
+ }
639
+ };
640
+ var FormValidator = class {
641
+ constructor(schema = {}) {
642
+ this.schema = schema;
643
+ this.errors = {};
644
+ this.touched = {};
645
+ }
646
+ /**
647
+ * Validate a single field
648
+ */
649
+ validateField(name, value, formData = {}) {
650
+ const fieldValidators = this.schema[name];
651
+ if (!fieldValidators) {
652
+ return null;
653
+ }
654
+ const validatorArray = Array.isArray(fieldValidators) ? fieldValidators : [fieldValidators];
655
+ for (const validator of validatorArray) {
656
+ const error = validator(value, formData);
657
+ if (error) {
658
+ return error;
659
+ }
660
+ }
661
+ return null;
662
+ }
663
+ /**
664
+ * Validate entire form
665
+ */
666
+ validate(formData) {
667
+ const errors = {};
668
+ let isValid = true;
669
+ for (const [name, value] of Object.entries(formData)) {
670
+ const error = this.validateField(name, value, formData);
671
+ if (error) {
672
+ errors[name] = error;
673
+ isValid = false;
674
+ }
675
+ }
676
+ for (const name of Object.keys(this.schema)) {
677
+ if (!(name in formData)) {
678
+ const error = this.validateField(name, void 0, formData);
679
+ if (error) {
680
+ errors[name] = error;
681
+ isValid = false;
682
+ }
683
+ }
684
+ }
685
+ this.errors = errors;
686
+ return { isValid, errors };
687
+ }
688
+ /**
689
+ * Mark field as touched
690
+ */
691
+ touch(name) {
692
+ this.touched[name] = true;
693
+ }
694
+ /**
695
+ * Check if field is touched
696
+ */
697
+ isTouched(name) {
698
+ return this.touched[name] || false;
699
+ }
700
+ /**
701
+ * Get error for field
702
+ */
703
+ getError(name) {
704
+ return this.errors[name] || null;
705
+ }
706
+ /**
707
+ * Check if field has error
708
+ */
709
+ hasError(name) {
710
+ return !!this.errors[name];
711
+ }
712
+ /**
713
+ * Clear errors
714
+ */
715
+ clearErrors() {
716
+ this.errors = {};
717
+ }
718
+ /**
719
+ * Clear touched state
720
+ */
721
+ clearTouched() {
722
+ this.touched = {};
723
+ }
724
+ /**
725
+ * Reset validator
726
+ */
727
+ reset() {
728
+ this.clearErrors();
729
+ this.clearTouched();
730
+ }
731
+ };
732
+ function createValidator(schema) {
733
+ return new FormValidator(schema);
734
+ }
735
+ function validate(formData, schema) {
736
+ const validator = new FormValidator(schema);
737
+ return validator.validate(formData);
738
+ }
739
+
740
+ // src/validators.js
741
+ var validators2 = {
479
742
  required: (value, options = {}) => {
480
743
  if (value === null || value === void 0 || value === "") {
481
- return options.message || validators.required.message || "This field is required";
744
+ return options.message || validators2.required.message || "This field is required";
482
745
  }
483
746
  return null;
484
747
  },
@@ -661,7 +924,7 @@ var validators = {
661
924
  },
662
925
  // Get a registered validator
663
926
  get: (name) => {
664
- return validators[name];
927
+ return validators2[name];
665
928
  },
666
929
  // Compose multiple validators
667
930
  compose: (validatorList) => {
@@ -729,19 +992,19 @@ var validators = {
729
992
  const stopOnFirstError = options.stopOnFirstError !== false;
730
993
  const chain = {
731
994
  required: (opts) => {
732
- validatorList.push((v, o, t, a) => validators.required(v, opts || o, t, a));
995
+ validatorList.push((v, o, t, a) => validators2.required(v, opts || o, t, a));
733
996
  return chain;
734
997
  },
735
998
  email: (opts) => {
736
- validatorList.push((v, o, t, a) => validators.email(v, opts || o, t, a));
999
+ validatorList.push((v, o, t, a) => validators2.email(v, opts || o, t, a));
737
1000
  return chain;
738
1001
  },
739
1002
  minLength: (opts) => {
740
- validatorList.push((v, o, t, a) => validators.minLength(v, opts || o, t, a));
1003
+ validatorList.push((v, o, t, a) => validators2.minLength(v, opts || o, t, a));
741
1004
  return chain;
742
1005
  },
743
1006
  maxLength: (opts) => {
744
- validatorList.push((v, o, t, a) => validators.maxLength(v, opts || o, t, a));
1007
+ validatorList.push((v, o, t, a) => validators2.maxLength(v, opts || o, t, a));
745
1008
  return chain;
746
1009
  },
747
1010
  custom: (fn, message) => {
@@ -796,6 +1059,7 @@ function validateForm(formData, fieldValidators) {
796
1059
  return Object.keys(errors).length > 0 ? errors : null;
797
1060
  }
798
1061
  function registerValidator(name, validatorFn) {
1062
+ validators2[name] = validatorFn;
799
1063
  validators[name] = validatorFn;
800
1064
  }
801
1065
  function composeValidators(...validatorFns) {
@@ -827,7 +1091,14 @@ function hydrateForm(formSelector, options = {}) {
827
1091
  validateOnSubmit: true,
828
1092
  showErrorsOnTouch: true,
829
1093
  debounce: 300,
830
- ...options
1094
+ ...options,
1095
+ // Must match whatever the server rendered, so pass the same names given to
1096
+ // buildForm's `classNames` when they were customised.
1097
+ classNames: {
1098
+ invalid: DEFAULT_CLASS_NAMES.invalid,
1099
+ error: DEFAULT_CLASS_NAMES.error,
1100
+ ...options.classNames
1101
+ }
831
1102
  };
832
1103
  const state = {
833
1104
  values: {},
@@ -842,8 +1113,8 @@ function hydrateForm(formSelector, options = {}) {
842
1113
  return validatorString.split(",").map((v) => {
843
1114
  const trimmed = v.trim();
844
1115
  const [name, ...params] = trimmed.split(":");
845
- if (validators[name]) {
846
- return params.length > 0 ? validators[name](...params.map((p) => isNaN(p) ? p : Number(p))) : validators[name];
1116
+ if (validators2[name]) {
1117
+ return params.length > 0 ? validators2[name](...params.map((p) => isNaN(p) ? p : Number(p))) : validators2[name];
847
1118
  }
848
1119
  return null;
849
1120
  }).filter(Boolean);
@@ -871,10 +1142,10 @@ function hydrateForm(formSelector, options = {}) {
871
1142
  function createErrorElement(name, inputElement) {
872
1143
  const errorDiv = document.createElement("div");
873
1144
  errorDiv.id = `${name}-error`;
874
- errorDiv.className = "error-message";
1145
+ if (opts.classNames.error) errorDiv.className = opts.classNames.error;
875
1146
  errorDiv.setAttribute("role", "alert");
876
1147
  errorDiv.style.display = "none";
877
- const fieldWrapper = inputElement.closest(".form-field") || inputElement.parentElement;
1148
+ const fieldWrapper = inputElement.closest("[data-field]") || inputElement.parentElement;
878
1149
  fieldWrapper.appendChild(errorDiv);
879
1150
  return errorDiv;
880
1151
  }
@@ -923,6 +1194,9 @@ function hydrateForm(formSelector, options = {}) {
923
1194
  displayError(name, error);
924
1195
  return !error;
925
1196
  }
1197
+ function invalidClasses() {
1198
+ return opts.classNames.invalid ? opts.classNames.invalid.trim().split(/\s+/) : [];
1199
+ }
926
1200
  function displayError(name, error) {
927
1201
  const field = state.fields.get(name);
928
1202
  if (!field) return;
@@ -931,12 +1205,12 @@ function hydrateForm(formSelector, options = {}) {
931
1205
  errorElement.textContent = error;
932
1206
  errorElement.style.display = "block";
933
1207
  element.setAttribute("aria-invalid", "true");
934
- element.classList.add("error");
1208
+ invalidClasses().forEach((name2) => element.classList.add(name2));
935
1209
  } else {
936
1210
  errorElement.textContent = "";
937
1211
  errorElement.style.display = "none";
938
1212
  element.setAttribute("aria-invalid", "false");
939
- element.classList.remove("error");
1213
+ invalidClasses().forEach((name2) => element.classList.remove(name2));
940
1214
  }
941
1215
  }
942
1216
  function validateForm2() {
@@ -1067,180 +1341,8 @@ function hydrateForm(formSelector, options = {}) {
1067
1341
  })
1068
1342
  };
1069
1343
  }
1070
-
1071
- // src/validation.js
1072
- var validators2 = {
1073
- required: (message = "This field is required") => (value) => {
1074
- if (value === null || value === void 0 || value === "") {
1075
- return message;
1076
- }
1077
- return null;
1078
- },
1079
- minLength: (min, message = `Minimum length is ${min}`) => (value) => {
1080
- if (value && value.length < min) {
1081
- return message;
1082
- }
1083
- return null;
1084
- },
1085
- maxLength: (max, message = `Maximum length is ${max}`) => (value) => {
1086
- if (value && value.length > max) {
1087
- return message;
1088
- }
1089
- return null;
1090
- },
1091
- min: (min, message = `Minimum value is ${min}`) => (value) => {
1092
- if (value !== null && value !== void 0 && Number(value) < min) {
1093
- return message;
1094
- }
1095
- return null;
1096
- },
1097
- max: (max, message = `Maximum value is ${max}`) => (value) => {
1098
- if (value !== null && value !== void 0 && Number(value) > max) {
1099
- return message;
1100
- }
1101
- return null;
1102
- },
1103
- email: (message = "Invalid email address") => (value) => {
1104
- if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
1105
- return message;
1106
- }
1107
- return null;
1108
- },
1109
- url: (message = "Invalid URL") => (value) => {
1110
- if (value) {
1111
- try {
1112
- new URL(value);
1113
- } catch {
1114
- return message;
1115
- }
1116
- }
1117
- return null;
1118
- },
1119
- pattern: (regex, message = "Invalid format") => (value) => {
1120
- if (value && !regex.test(value)) {
1121
- return message;
1122
- }
1123
- return null;
1124
- },
1125
- matches: (fieldName, message = "Fields do not match") => (value, formData) => {
1126
- if (value !== formData[fieldName]) {
1127
- return message;
1128
- }
1129
- return null;
1130
- },
1131
- oneOf: (options, message = "Invalid option") => (value) => {
1132
- if (value && !options.includes(value)) {
1133
- return message;
1134
- }
1135
- return null;
1136
- },
1137
- custom: (fn, message = "Validation failed") => (value, formData) => {
1138
- if (!fn(value, formData)) {
1139
- return message;
1140
- }
1141
- return null;
1142
- }
1143
- };
1144
- var FormValidator = class {
1145
- constructor(schema = {}) {
1146
- this.schema = schema;
1147
- this.errors = {};
1148
- this.touched = {};
1149
- }
1150
- /**
1151
- * Validate a single field
1152
- */
1153
- validateField(name, value, formData = {}) {
1154
- const fieldValidators = this.schema[name];
1155
- if (!fieldValidators) {
1156
- return null;
1157
- }
1158
- const validatorArray = Array.isArray(fieldValidators) ? fieldValidators : [fieldValidators];
1159
- for (const validator of validatorArray) {
1160
- const error = validator(value, formData);
1161
- if (error) {
1162
- return error;
1163
- }
1164
- }
1165
- return null;
1166
- }
1167
- /**
1168
- * Validate entire form
1169
- */
1170
- validate(formData) {
1171
- const errors = {};
1172
- let isValid = true;
1173
- for (const [name, value] of Object.entries(formData)) {
1174
- const error = this.validateField(name, value, formData);
1175
- if (error) {
1176
- errors[name] = error;
1177
- isValid = false;
1178
- }
1179
- }
1180
- for (const name of Object.keys(this.schema)) {
1181
- if (!(name in formData)) {
1182
- const error = this.validateField(name, void 0, formData);
1183
- if (error) {
1184
- errors[name] = error;
1185
- isValid = false;
1186
- }
1187
- }
1188
- }
1189
- this.errors = errors;
1190
- return { isValid, errors };
1191
- }
1192
- /**
1193
- * Mark field as touched
1194
- */
1195
- touch(name) {
1196
- this.touched[name] = true;
1197
- }
1198
- /**
1199
- * Check if field is touched
1200
- */
1201
- isTouched(name) {
1202
- return this.touched[name] || false;
1203
- }
1204
- /**
1205
- * Get error for field
1206
- */
1207
- getError(name) {
1208
- return this.errors[name] || null;
1209
- }
1210
- /**
1211
- * Check if field has error
1212
- */
1213
- hasError(name) {
1214
- return !!this.errors[name];
1215
- }
1216
- /**
1217
- * Clear errors
1218
- */
1219
- clearErrors() {
1220
- this.errors = {};
1221
- }
1222
- /**
1223
- * Clear touched state
1224
- */
1225
- clearTouched() {
1226
- this.touched = {};
1227
- }
1228
- /**
1229
- * Reset validator
1230
- */
1231
- reset() {
1232
- this.clearErrors();
1233
- this.clearTouched();
1234
- }
1235
- };
1236
- function createValidator(schema) {
1237
- return new FormValidator(schema);
1238
- }
1239
- function validate(formData, schema) {
1240
- const validator = new FormValidator(schema);
1241
- return validator.validate(formData);
1242
- }
1243
1344
  export {
1345
+ DEFAULT_CLASS_NAMES,
1244
1346
  FormBuilder,
1245
1347
  FormValidator,
1246
1348
  buildForm,
@@ -1252,6 +1354,6 @@ export {
1252
1354
  validate,
1253
1355
  validateField,
1254
1356
  validateForm,
1255
- validators2 as validators
1357
+ validators
1256
1358
  };
1257
1359
  //# sourceMappingURL=index.js.map