@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/types/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module @coherent.js/forms
4
4
  */
5
5
 
6
- import type { CoherentNode, CoherentElement, StrictCoherentElement } from '@coherent.js/core';
6
+ import type { CoherentNode } from '@coherent.js/core';
7
7
 
8
8
  // ============================================================================
9
9
  // Form Field Types
@@ -58,6 +58,10 @@ export interface FormField<T = unknown> {
58
58
  placeholder?: string;
59
59
  /** Whether field is required */
60
60
  required?: boolean;
61
+ /** Render the field; `false` omits it from the built form */
62
+ visible?: boolean;
63
+ /** Render the field only when this returns true for the current values */
64
+ showWhen?: (values: Record<string, unknown>) => boolean;
61
65
  /** Whether field is disabled */
62
66
  disabled?: boolean;
63
67
  /** Whether field is readonly */
@@ -72,8 +76,17 @@ export interface FormField<T = unknown> {
72
76
  validators?: Validator[];
73
77
  /** Field-specific validation configuration */
74
78
  validation?: FieldValidation<T>;
75
- /** Additional HTML attributes */
79
+ /**
80
+ * Extra attributes on the control, for things the field config has no
81
+ * dedicated option for — `autocomplete`, `maxlength`, `tabindex`, `data-*`.
82
+ *
83
+ * Applied before the builder's own attributes, so `name`, `id`, `type` and
84
+ * the `aria-*` pair cannot be overridden. Names that are not valid HTML
85
+ * attribute names are dropped with a warning.
86
+ */
76
87
  attributes?: Record<string, unknown>;
88
+ /** Class on the control, appended to `classNames.control` */
89
+ className?: string;
77
90
  /** Transform function to convert raw input to typed value */
78
91
  transform?: (value: unknown) => T;
79
92
  }
@@ -109,84 +122,173 @@ export interface FieldValidation<T = unknown> {
109
122
  // Form Builder Types
110
123
  // ============================================================================
111
124
 
125
+ /**
126
+ * Class applied to each structural slot of a built form.
127
+ *
128
+ * The wrapper is found by hydration through its `data-field` attribute, not
129
+ * its class, so every one of these is free for the consumer to choose.
130
+ */
131
+ export interface FormClassNames {
132
+ /** Wrapper around label, control and error */
133
+ field: string;
134
+ label: string;
135
+ /** Base class on the control, before any per-field `className` */
136
+ control: string;
137
+ /** Added to the control while it has a visible error */
138
+ invalid: string;
139
+ /** The error message element */
140
+ error: string;
141
+ submit: string;
142
+ }
143
+
144
+ /** The class names a form uses when `classNames` does not override them. */
145
+ export const DEFAULT_CLASS_NAMES: FormClassNames;
146
+
112
147
  /**
113
148
  * Form configuration options
114
149
  */
115
150
  export interface FormConfig {
116
- /** Form fields */
117
- fields?: FormField[];
151
+ /** Fields, as an array of `{ name, ...config }` or keyed by field name */
152
+ fields?: FormField[] | Record<string, Omit<FormField, 'name'>>;
118
153
  /** Form action URL */
119
154
  action?: string;
120
155
  /** Form submission method */
121
- method?: 'get' | 'post';
156
+ method?: 'get' | 'post' | (string & {});
157
+ /** Form name attribute; defaults to `'form'` */
158
+ name?: string;
122
159
  /** Form CSS class name */
123
160
  className?: string;
124
161
  /** Submit button text */
125
162
  submitText?: string;
126
163
  /** Form submit handler */
127
- onSubmit?: (data: FormData) => void | Promise<void>;
164
+ onSubmit?: (data: Record<string, unknown>) => void | Promise<void>;
128
165
  /** Form encoding type */
129
166
  enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
130
- /** Whether to disable browser validation */
167
+ /**
168
+ * Emit `novalidate`, turning off the browser's own validation. Defaults to
169
+ * `false` — leaving native validation working with JavaScript disabled.
170
+ */
131
171
  novalidate?: boolean;
172
+ /**
173
+ * Emit an inline `onsubmit` handler. `true` uses `handleSubmit(event)`; a
174
+ * string is used verbatim. Off by default, so the form submits natively —
175
+ * {@link hydrateForm} binds its own listener and needs nothing inline, and
176
+ * an inline handler is blocked by a strict CSP.
177
+ */
178
+ enhance?: boolean | string;
179
+ /** Class names for each structural slot; see {@link DEFAULT_CLASS_NAMES} */
180
+ classNames?: Partial<FormClassNames>;
132
181
  /** Form ID */
133
182
  id?: string;
183
+ /** Validate a field as it changes; defaults to `true` */
184
+ validateOnChange?: boolean;
185
+ /** Validate a field when it loses focus; defaults to `true` */
186
+ validateOnBlur?: boolean;
187
+ [option: string]: unknown;
134
188
  }
135
189
 
136
190
  /**
137
- * Typed form builder with generic form data shape
191
+ * Accumulates fields and renders them as a Coherent component.
192
+ *
193
+ * Every mutator is chainable, and `build()`, `render()` and `buildForm()` all
194
+ * return the same node — `toHTML()` is that node rendered to a string.
195
+ *
196
+ * ```ts
197
+ * const form = new FormBuilder({ name: 'signup' })
198
+ * .field('email', { type: 'email', label: 'Email', required: true })
199
+ * .setAction('/subscribe')
200
+ * .setMethod('post')
201
+ * .build();
202
+ * ```
203
+ *
138
204
  * @template T - The shape of the form data
139
205
  */
140
- export interface FormBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
141
- /**
142
- * Add a field to the form
143
- * @template K - The key in the form data
144
- */
145
- addField<K extends keyof T>(name: K, field: Omit<FormField<T[K]>, 'name'>): FormBuilder<T>;
146
-
147
- /**
148
- * Remove a field from the form
149
- */
150
- removeField(name: keyof T): FormBuilder<T>;
206
+ export class FormBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
207
+ constructor(options?: FormConfig);
208
+
209
+ options: FormConfig;
210
+ fields: Map<string, FormField>;
211
+ values: Partial<T>;
212
+ errors: Record<string, string>;
213
+ touched: Record<string, boolean>;
214
+
215
+ /** Define a field */
216
+ field<K extends keyof T & string>(name: K, config?: Omit<FormField<T[K]>, 'name'>): this;
217
+ /** Alias of {@link FormBuilder.field} */
218
+ addField<K extends keyof T & string>(name: K, config?: Omit<FormField<T[K]>, 'name'>): this;
219
+ removeField(name: keyof T & string): this;
220
+ /** Merge changes into an existing field */
221
+ updateField(name: keyof T & string, config: Partial<FormField>): this;
222
+
223
+ /** Every field definition, in insertion order */
224
+ getFields(): FormField[];
225
+ getField(name: keyof T & string): FormField | undefined;
151
226
 
152
- /**
153
- * Set the form action URL
154
- */
155
- setAction(action: string): FormBuilder<T>;
227
+ /** Group fields for layout */
228
+ addGroup(name: string, config?: Record<string, unknown>): this;
229
+ getGroup(name: string): Record<string, unknown> | undefined;
156
230
 
157
- /**
158
- * Set the form submission method
159
- */
160
- setMethod(method: 'get' | 'post'): FormBuilder<T>;
231
+ setValue<K extends keyof T & string>(name: K, value: T[K]): this;
232
+ setValues(values: Partial<T>): this;
233
+ getValue<K extends keyof T & string>(name: K): T[K] | undefined;
234
+ getValues(): Partial<T>;
161
235
 
162
- /**
163
- * Set the form submit handler
164
- */
165
- onSubmit(handler: (data: T) => void | Promise<void>): FormBuilder<T>;
236
+ /** Run one field's validators; returns the error or `null` */
237
+ validateField(name: keyof T & string): string | null;
166
238
 
167
239
  /**
168
- * Build the form as a CoherentNode
240
+ * Validate every visible field and store the result. Returns the errors
241
+ * keyed by field name — empty when the form is valid.
169
242
  */
170
- build(): CoherentNode;
243
+ validate(): Record<string, string>;
171
244
 
172
- /**
173
- * Render the form (alias for build)
174
- */
175
- render(): CoherentNode;
245
+ getFieldError(name: keyof T & string): string | null;
246
+ hasErrors(): boolean;
247
+ clearErrors(): this;
248
+ isValid(): boolean;
249
+ /** Mark a field as touched */
250
+ touch(name: keyof T & string): void;
251
+ /** Whether any value differs from its initial value */
252
+ isDirty(): boolean;
176
253
 
177
- /**
178
- * Validate form data
179
- */
180
- validate(data: unknown): { valid: boolean; errors: Record<keyof T, string[]> };
254
+ onSubmit(handler: (data: Partial<T>) => void | Promise<void>): this;
255
+ onError(handler: (error: unknown) => void): this;
256
+ isSubmitting(): boolean;
181
257
 
182
- /**
183
- * Get current field definitions
184
- */
185
- getFields(): FormField[];
258
+ setAction(action: string): this;
259
+ setMethod(method: 'get' | 'post' | (string & {})): this;
260
+
261
+ /** Build the form component */
262
+ buildForm(options?: FormConfig): CoherentNode;
263
+ /** Alias of {@link FormBuilder.buildForm} */
264
+ build(options?: FormConfig): CoherentNode;
265
+ /** Alias of {@link FormBuilder.buildForm} */
266
+ render(options?: FormConfig): CoherentNode;
267
+ /** The built form, rendered to an HTML string */
268
+ toHTML(options?: FormConfig): string;
269
+
270
+ /** Merge configured class names over {@link DEFAULT_CLASS_NAMES} */
271
+ resolveClassNames(overrides?: Partial<FormClassNames>): FormClassNames;
272
+
273
+ /** Build the node for one field, including its label and error */
274
+ buildField(name: keyof T & string, classNames?: FormClassNames): CoherentNode;
275
+ /** Build one field's control */
276
+ buildInput(name: keyof T & string, classNames?: FormClassNames): CoherentNode | null;
277
+ /** Build one field's label */
278
+ buildLabel(name: keyof T & string, classNames?: FormClassNames): CoherentNode | null;
279
+ /** Build one field's error message, or `null` when it has none to show */
280
+ buildError(name: keyof T & string, classNames?: FormClassNames): CoherentNode | null;
281
+
282
+ /** Copy of the current values */
283
+ serialize(): Partial<T>;
284
+ /** Whether a field's `showWhen`/`showIf` condition currently holds */
285
+ isFieldVisible(name: keyof T & string): boolean;
286
+ /** Restore default values and clear errors and touched state */
287
+ reset(): this;
186
288
  }
187
289
 
188
290
  /**
189
- * Create a typed form builder
291
+ * Create a form builder, optionally seeding it from `config.fields`.
190
292
  * @template T - The shape of the form data
191
293
  */
192
294
  export function createFormBuilder<T extends Record<string, unknown> = Record<string, unknown>>(
@@ -194,35 +296,12 @@ export function createFormBuilder<T extends Record<string, unknown> = Record<str
194
296
  ): FormBuilder<T>;
195
297
 
196
298
  /**
197
- * Build a form from configuration
198
- */
199
- export function buildForm(config: FormConfig): CoherentNode;
200
-
201
- /**
202
- * Validate a single field value
203
- * @returns Error message or null if valid
299
+ * Build a form component from configuration, in one call.
300
+ *
301
+ * `fields` may be an array of `{ name, ...config }` objects or an object
302
+ * keyed by field name; passing a bare array is shorthand for `{ fields }`.
204
303
  */
205
- export function validateField<T>(field: FormField<T>, value: unknown): string | null;
206
-
207
- // ============================================================================
208
- // Form Builder Class
209
- // ============================================================================
210
-
211
- /**
212
- * Form builder class implementation
213
- */
214
- export class FormBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
215
- constructor(config?: FormConfig);
216
- addField<K extends keyof T>(name: K, field: Omit<FormField<T[K]>, 'name'>): this;
217
- removeField(name: keyof T): this;
218
- setAction(action: string): this;
219
- setMethod(method: 'get' | 'post'): this;
220
- onSubmit(handler: (data: T) => void | Promise<void>): this;
221
- build(): CoherentNode;
222
- render(): CoherentNode;
223
- validate(data: unknown): { valid: boolean; errors: Record<keyof T, string[]> };
224
- getFields(): FormField[];
225
- }
304
+ export function buildForm(config?: FormConfig | FormField[]): CoherentNode;
226
305
 
227
306
  // ============================================================================
228
307
  // Form Hydration Types
@@ -232,51 +311,81 @@ export class FormBuilder<T extends Record<string, unknown> = Record<string, unkn
232
311
  * Options for hydrating a form on the client
233
312
  */
234
313
  export interface HydrationOptions {
235
- /** Enable validation */
236
- validation?: boolean;
237
- /** Enable real-time validation as user types */
238
- realTimeValidation?: boolean;
239
- /** Form submit handler */
240
- onSubmit?: (event: Event, data: FormData) => void | Promise<void>;
241
- /** Validation error handler */
242
- onValidate?: (errors: ValidationErrors) => void;
243
- /** Prevent default form submission */
244
- preventSubmit?: boolean;
314
+ /** Validate a field when it loses focus; defaults to `true` */
315
+ validateOnBlur?: boolean;
316
+ /** Validate a field as it changes; defaults to `false` */
317
+ validateOnChange?: boolean;
318
+ /** Validate everything on submit; defaults to `true` */
319
+ validateOnSubmit?: boolean;
320
+ /** Only show a field's error once it has been touched; defaults to `true` */
321
+ showErrorsOnTouch?: boolean;
322
+ /** Debounce window for change validation, in ms; defaults to `300` */
323
+ debounce?: number;
324
+ /**
325
+ * Class names the client writes. Pass the same `invalid` and `error` given
326
+ * to {@link buildForm}, or the classes added on failure will not match what
327
+ * the server rendered.
328
+ */
329
+ classNames?: Partial<Pick<FormClassNames, 'invalid' | 'error'>>;
330
+ /**
331
+ * Called instead of the browser's native submit. Return `false` to cancel,
332
+ * or a promise to defer completion.
333
+ */
334
+ onSubmit?: (data: Record<string, unknown>, event: Event) => unknown;
335
+ /** Called with the field errors on a failed submit, or a rejected `onSubmit` */
336
+ onError?: (errors: ValidationErrors | unknown) => void;
337
+ /** Called after a promise returned by `onSubmit` resolves */
338
+ onSuccess?: (data: Record<string, unknown>) => void;
339
+ [option: string]: unknown;
245
340
  }
246
341
 
247
342
  /**
248
- * Hydrated form interface for client-side interaction
343
+ * Controller returned by {@link hydrateForm}.
249
344
  */
250
345
  export interface HydratedForm {
251
- /** The form DOM element */
252
- element: HTMLFormElement;
253
- /** Validate all form fields */
254
- validate(): ValidationResult;
255
- /** Reset form to initial values */
346
+ /** Validate one field and record the result */
347
+ validateField(name: string): string | null;
348
+ /** Validate every field; `true` when all pass */
349
+ validateForm(): boolean;
350
+
351
+ setFieldValue(name: string, value: unknown): void;
352
+ getFieldValue(name: string): unknown;
353
+
354
+ /** Error currently shown for a field, or `undefined` */
355
+ getError(name: string): string | undefined;
356
+ /** Copy of the current errors */
357
+ getErrors(): ValidationErrors;
358
+ /** Copy of the current values */
359
+ getValues(): Record<string, unknown>;
360
+
361
+ setTouched(name: string, touched?: boolean): void;
362
+
363
+ /** Restore initial values and clear errors */
256
364
  reset(): void;
257
- /** Get current form data */
258
- getData(): FormData;
259
- /** Get form data as object */
260
- getValues<T = Record<string, unknown>>(): T;
261
- /** Set form field values */
262
- setData(data: Record<string, unknown>): void;
263
- /** Set a single field value */
264
- setValue(name: string, value: unknown): void;
265
- /** Destroy hydration and clean up event listeners */
365
+ /** Detach every listener and cancel pending debounces */
266
366
  destroy(): void;
267
- /** Check if form is valid */
367
+
268
368
  isValid(): boolean;
269
- /** Get validation errors */
270
- getErrors(): ValidationErrors;
369
+ isSubmitting(): boolean;
370
+
371
+ /** Snapshot of values, errors, touched flags and submit state */
372
+ getState(): {
373
+ values: Record<string, unknown>;
374
+ errors: ValidationErrors;
375
+ touched: Record<string, boolean>;
376
+ isSubmitting: boolean;
377
+ };
271
378
  }
272
379
 
273
380
  /**
274
- * Hydrate a form element for client-side interactivity
381
+ * Attach client-side behavior to a server-rendered form.
382
+ *
383
+ * Returns `null` outside a browser, or when the selector matches nothing.
275
384
  */
276
385
  export function hydrateForm(
277
- formElement: HTMLFormElement | string,
386
+ formSelector: HTMLFormElement | string,
278
387
  options?: HydrationOptions
279
- ): HydratedForm;
388
+ ): HydratedForm | null;
280
389
 
281
390
  // ============================================================================
282
391
  // Validation Types
@@ -286,9 +395,9 @@ export function hydrateForm(
286
395
  * Validation result
287
396
  */
288
397
  export interface ValidationResult {
289
- /** Whether all fields are valid */
290
- valid: boolean;
291
- /** Validation errors by field name */
398
+ /** Whether every field passed */
399
+ isValid: boolean;
400
+ /** The first error per failing field; passing fields are absent */
292
401
  errors: ValidationErrors;
293
402
  }
294
403
 
@@ -296,86 +405,159 @@ export interface ValidationResult {
296
405
  * Validation errors mapped by field name
297
406
  */
298
407
  export interface ValidationErrors {
299
- [fieldName: string]: string[];
408
+ [fieldName: string]: string;
300
409
  }
301
410
 
302
411
  /**
303
- * Validator function type
412
+ * A field check: returns an error message, or `null` when the value passes.
413
+ *
414
+ * The second argument is the whole form, so validators like
415
+ * `validators.matches` can compare fields.
304
416
  */
305
417
  export interface Validator {
306
- (value: unknown): boolean | string | Promise<boolean | string>;
418
+ (value: unknown, formData?: Record<string, unknown>): string | null;
307
419
  }
308
420
 
309
421
  /**
310
- * Form validator class
422
+ * Per-field validators. A field maps to one validator or a list run in order,
423
+ * stopping at the first error.
424
+ */
425
+ export type ValidationSchema = Record<string, Validator | Validator[]>;
426
+
427
+ /**
428
+ * Runs a {@link ValidationSchema} and tracks errors and touched fields.
429
+ *
430
+ * ```ts
431
+ * const validator = new FormValidator({
432
+ * email: [validators.required(), validators.email()]
433
+ * });
434
+ * const { isValid, errors } = validator.validate({ email: '' });
435
+ * ```
311
436
  */
312
437
  export class FormValidator {
313
- constructor(rules: Record<string, Validator[]>);
314
- /** Synchronous validation */
315
- validate(data: Record<string, unknown>): ValidationResult;
316
- /** Asynchronous validation */
317
- validateAsync(data: Record<string, unknown>): Promise<ValidationResult>;
318
- /** Add a validation rule */
319
- addRule(field: string, validator: Validator): void;
320
- /** Remove a validation rule */
321
- removeRule(field: string, validator: Validator): void;
322
- /** Clear all rules for a field */
323
- clearRules(field: string): void;
438
+ constructor(schema?: ValidationSchema);
439
+
440
+ schema: ValidationSchema;
441
+ /** Errors from the last `validate()` call */
442
+ errors: ValidationErrors;
443
+ touched: Record<string, boolean>;
444
+
445
+ /**
446
+ * Check one field. Returns the first error, or `null` when it passes or has
447
+ * no validators.
448
+ */
449
+ validateField(
450
+ name: string,
451
+ value: unknown,
452
+ formData?: Record<string, unknown>
453
+ ): string | null;
454
+
455
+ /**
456
+ * Check every field in `formData` plus any schema field it omits, and store
457
+ * the result in `errors`.
458
+ */
459
+ validate(formData: Record<string, unknown>): ValidationResult;
460
+
461
+ /** Mark a field as touched */
462
+ touch(name: string): void;
463
+ isTouched(name: string): boolean;
464
+
465
+ /** Error recorded for a field by the last `validate()`, or `null` */
466
+ getError(name: string): string | null;
467
+ hasError(name: string): boolean;
468
+
469
+ clearErrors(): void;
470
+ clearTouched(): void;
471
+ /** Clear both errors and touched state */
472
+ reset(): void;
324
473
  }
325
474
 
326
475
  /**
327
476
  * Create a form validator
328
477
  */
329
- export function createValidator(rules: Record<string, Validator[]>): FormValidator;
478
+ export function createValidator(schema?: ValidationSchema): FormValidator;
330
479
 
331
480
  /**
332
- * Validate data against rules
481
+ * Validate data against a schema with a throwaway validator
333
482
  */
334
483
  export function validate(
335
- data: Record<string, unknown>,
336
- rules: Record<string, Validator[]>
484
+ formData: Record<string, unknown>,
485
+ schema?: ValidationSchema
337
486
  ): ValidationResult;
338
487
 
488
+ /**
489
+ * Run a list of validators against one value, returning the first error or
490
+ * `null`.
491
+ */
492
+ export function validateField(
493
+ value: unknown,
494
+ validatorList: Validator[],
495
+ formData?: Record<string, unknown>
496
+ ): string | null;
497
+
498
+ /**
499
+ * Run per-field validator lists over a whole form. Returns `null` when
500
+ * everything passes, rather than an empty object.
501
+ */
502
+ export function validateForm(
503
+ formData: Record<string, unknown>,
504
+ fieldValidators: Record<string, Validator[]>
505
+ ): ValidationErrors | null;
506
+
507
+ /**
508
+ * Add a validator to {@link validators} under `name`.
509
+ *
510
+ * Unlike the built-ins, which are factories, this stores `validatorFn`
511
+ * directly — so use it as `validators[name]`, not `validators[name]()`.
512
+ * Registering over a built-in therefore changes that name's calling
513
+ * convention.
514
+ */
515
+ export function registerValidator(name: string, validatorFn: Validator): void;
516
+
517
+ /**
518
+ * Combine validators into one that returns the first error, or `null`.
519
+ */
520
+ export function composeValidators(...validatorFns: Validator[]): Validator;
521
+
339
522
  // ============================================================================
340
523
  // Built-in Validators
341
524
  // ============================================================================
342
525
 
343
526
  /**
344
- * Built-in validator functions
527
+ * Built-in validator factories. Each returns a {@link Validator}, so call it
528
+ * before putting it in a schema: `validators.required()`, not
529
+ * `validators.required`.
530
+ *
531
+ * Validators added with {@link registerValidator} also appear here, but are
532
+ * stored as bare validators rather than factories.
345
533
  */
346
534
  export const validators: {
347
- /** Require a value to be present */
535
+ /** Reject `null`, `undefined` and the empty string */
348
536
  required(message?: string): Validator;
349
- /** Validate email format */
537
+ /** Validate email format; empty values pass */
350
538
  email(message?: string): Validator;
351
- /** Minimum string length */
352
- minLength(length: number, message?: string): Validator;
353
- /** Maximum string length */
354
- maxLength(length: number, message?: string): Validator;
539
+ /** Minimum length; empty values pass */
540
+ minLength(min: number, message?: string): Validator;
541
+ /** Maximum length; empty values pass */
542
+ maxLength(max: number, message?: string): Validator;
355
543
  /** Minimum numeric value */
356
- min(value: number, message?: string): Validator;
544
+ min(min: number, message?: string): Validator;
357
545
  /** Maximum numeric value */
358
- max(value: number, message?: string): Validator;
359
- /** Pattern matching */
360
- pattern(regex: RegExp, message?: string): Validator;
361
- /** Match another field's value */
362
- matches(field: string, message?: string): Validator;
363
- /** Validate URL format */
546
+ max(max: number, message?: string): Validator;
547
+ /** Parseable as a URL; empty values pass */
364
548
  url(message?: string): Validator;
365
- /** Validate as number */
366
- number(message?: string): Validator;
367
- /** Validate as integer */
368
- integer(message?: string): Validator;
369
- /** Validate as positive number */
370
- positive(message?: string): Validator;
371
- /** Validate as negative number */
372
- negative(message?: string): Validator;
373
- /** Validate date format */
374
- date(message?: string): Validator;
375
- /** Custom validation function */
376
- custom(fn: (value: unknown) => boolean | string, message?: string): Validator;
377
- /** Async validation function */
378
- async(fn: (value: unknown) => Promise<boolean | string>): Validator;
549
+ /** Match a regular expression; empty values pass */
550
+ pattern(regex: RegExp, message?: string): Validator;
551
+ /** Equal another field's value */
552
+ matches(fieldName: string, message?: string): Validator;
553
+ /** One of a fixed set; empty values pass */
554
+ oneOf(options: unknown[], message?: string): Validator;
555
+ /** Fail when `fn` returns falsy */
556
+ custom(
557
+ fn: (value: unknown, formData?: Record<string, unknown>) => boolean,
558
+ message?: string
559
+ ): Validator;
560
+ [name: string]: Validator | ((...args: never[]) => Validator);
379
561
  };
380
562
 
381
563
  // ============================================================================