@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/form-builder.js +159 -68
- package/dist/form-builder.js.map +2 -2
- package/dist/form-hydration.js +30 -5
- package/dist/form-hydration.js.map +4 -4
- package/dist/index.js +360 -258
- package/dist/index.js.map +4 -4
- package/dist/validators.js +84 -9
- package/dist/validators.js.map +4 -4
- package/package.json +5 -4
- package/types/index.d.ts +337 -155
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
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
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(
|
|
552
|
+
function buildForm(config = {}) {
|
|
553
|
+
const { fields = [], ...options } = Array.isArray(config) ? { fields: config } : config;
|
|
470
554
|
const builder = new FormBuilder(options);
|
|
471
|
-
|
|
472
|
-
|
|
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/
|
|
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 ||
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 (
|
|
846
|
-
return params.length > 0 ?
|
|
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 =
|
|
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("
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1357
|
+
validators
|
|
1256
1358
|
};
|
|
1257
1359
|
//# sourceMappingURL=index.js.map
|