@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/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module @coherent.js/forms
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { CoherentNode
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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:
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
243
|
+
validate(): Record<string, string>;
|
|
171
244
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
|
|
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
|
-
*
|
|
343
|
+
* Controller returned by {@link hydrateForm}.
|
|
249
344
|
*/
|
|
250
345
|
export interface HydratedForm {
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
/** Validate all
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
367
|
+
|
|
268
368
|
isValid(): boolean;
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
290
|
-
|
|
291
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
418
|
+
(value: unknown, formData?: Record<string, unknown>): string | null;
|
|
307
419
|
}
|
|
308
420
|
|
|
309
421
|
/**
|
|
310
|
-
*
|
|
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(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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(
|
|
478
|
+
export function createValidator(schema?: ValidationSchema): FormValidator;
|
|
330
479
|
|
|
331
480
|
/**
|
|
332
|
-
* Validate data against
|
|
481
|
+
* Validate data against a schema with a throwaway validator
|
|
333
482
|
*/
|
|
334
483
|
export function validate(
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
352
|
-
minLength(
|
|
353
|
-
/** Maximum
|
|
354
|
-
maxLength(
|
|
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(
|
|
544
|
+
min(min: number, message?: string): Validator;
|
|
357
545
|
/** Maximum numeric value */
|
|
358
|
-
max(
|
|
359
|
-
/**
|
|
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
|
-
/**
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
// ============================================================================
|