@@ -13,66 +13,88 @@ import {
13
13
/**
14
14
* A custom hook to manage form state, validation, and submission handling.
15
15
*
16
- * @template T - A record type representing form data.
17
- * @param options (optional) - Options for initial form values, validation rules, and form mode.
18
- * @param options.validations - An object containing validation rules for form fields.
19
- * @param options.initialValues - An object representing the initial values of the form.
20
- * @param options.mode (default: `onChange`) - A string to set validation mode, either 'onChange' or 'onBlur'.
21
- * @returns Returns form state, handlers for change and submission, validation errors, and a trigger function for manual validation.
16
+ * @param options - Optional configuration object for the form.
17
+ * @returns The form state and utility methods
22
18
*/
23
19
export const useForm = < T extends Record < keyof T , any > = { } > ( options ?: {
20
+ /** An object containing validation rules for each form field. */
24
21
validations ?: UseFormValidations < T >
22
+ /** An object representing the initial values for the form fields. */
25
23
initialValues ?: Partial < T >
26
- mode ?: 'onChange' | 'onBlur'
24
+ /** Defines when validation should occur:
25
+ * - 'onChange': Validation occurs when the user modifies the input
26
+ * - 'onBlur': Validation occurs when the input loses focus.
27
+ * @default 'onChange'
28
+ */
29
+ validationMode ?: 'onChange' | 'onBlur'
27
30
} ) => {
28
31
const [ data , setData ] = useState < T > ( ( options ?. initialValues || { } ) as T )
29
32
const [ dirtyFields , setDirtyFields ] = useState < DirtyFields < T > > ( { } )
30
33
const [ touchedFields , setTouchedFields ] = useState < TouchedFields < T > > ( { } )
31
34
const [ errors , setErrors ] = useState < UseFormErrors < T > > ( { } )
35
+ const [ enableValidationOnChange , setEnableValidationOnChange ] = useState < Partial < Record < keyof T , boolean > > > ( { } )
32
36
33
37
/**
34
- * Handles change events for form inputs , updates the form data, and triggers validation.
38
+ * Handles change events for form fields , updates the form data, and triggers validation.
35
39
*
36
- * @template S - The sanitized value type.
37
40
* @param key - The key of the form field to be updated.
38
41
* @param sanitizeFn - An optional function to sanitize the input value.
39
42
* @returns The event handler for input changes.
40
43
*/
41
44
const onChange =
42
- < S extends unknown > ( key : keyof T , sanitizeFn ?: ( value : string ) => S ) =>
43
- ( e : ChangeEvent < HTMLInputElement & HTMLSelectElement > ) => {
44
- const value = sanitizeFn ? sanitizeFn ( e . target . value ) : e . target . value
45
+ < V extends unknown = string , S extends unknown = unknown > ( key : keyof T , sanitizeFn ?: ( value : V ) => S ) =>
46
+ // TODO: add support for `Checkbox`, `SelectPicker` and `RadioGroup` components
47
+ ( e : ChangeEvent < HTMLInputElement > ) => {
48
+ const value = sanitizeFn ? sanitizeFn ( e . target . value as V ) : e . target . value
45
49
setData ( {
46
50
...data ,
47
51
[ key ] : value ,
48
52
} )
49
- setTouchedFields ( {
50
- ...data ,
51
- [ key ] : true ,
52
- } )
53
+ const initialValues : Partial < T > = options ?. initialValues ?? { }
54
+ setDirtyFields ( { ...dirtyFields , [ key ] : initialValues [ key ] === data [ key ] } )
53
55
54
- if ( ! options ?. mode || options ?. mode === 'onChange' || dirtyFields [ key ] ) {
56
+ const validationMode = options ?. validationMode ?? 'onChange'
57
+ if ( validationMode === 'onChange' || enableValidationOnChange [ key ] || errors [ key ] ) {
55
58
const validations = options ?. validations ?? { }
56
59
const error = checkValidation < T > ( value as T [ keyof T ] , validations [ key as string ] )
57
60
setErrors ( { ...errors , [ key ] : error } )
58
61
}
59
62
}
60
63
61
64
/**
62
- * Handles blur events for form inputs and triggers validation if the form mode is 'onBlur'.
65
+ * Handles blur events for form fields and triggers validation if the form mode is 'onBlur'.
63
66
*
64
- * @param key - The key of the form field to be validated on blur .
67
+ * @param key - The key of the form field.
65
68
* @returns The event handler for the blur event.
66
69
*/
67
- const onBlur = ( key : keyof T ) => ( ) => {
68
- setDirtyFields ( { ...dirtyFields , [ key ] : options ?. initialValues ?. [ key ] === data [ key ] } )
69
- if ( options ?. mode === 'onBlur' ) {
70
+ const onBlur = ( key : keyof T , noTrim : boolean ) => ( ) => {
71
+ if ( ! noTrim ) {
72
+ setData ( { ...data , [ key ] : data [ key ] . trim ( ) } )
73
+ }
74
+
75
+ if ( options ?. validationMode === 'onBlur' ) {
70
76
const validations = options ?. validations ?? { }
71
77
const error = checkValidation < T > ( data [ key ] as T [ keyof T ] , validations [ key as string ] )
78
+ if ( error && ! enableValidationOnChange [ key ] ) {
79
+ setEnableValidationOnChange ( { ...enableValidationOnChange , [ key ] : true } )
80
+ }
72
81
setErrors ( { ...errors , [ key ] : error } )
73
82
}
74
83
}
75
84
85
+ /**
86
+ * Handles the focus event for form fields and updates the `touchedFields` state to mark the field as touched.
87
+ *
88
+ * @param key - The key of the form field.
89
+ * @return The event handler for the focus event.
90
+ */
91
+ const onFocus = ( key : keyof T ) => ( ) => {
92
+ setTouchedFields ( {
93
+ ...data ,
94
+ [ key ] : true ,
95
+ } )
96
+ }
97
+
76
98
/**
77
99
* Handles form submission, validates all form fields, and calls the provided `onValid` function if valid.
78
100
*
@@ -81,6 +103,12 @@ export const useForm = <T extends Record<keyof T, any> = {}>(options?: {
81
103
*/
82
104
const handleSubmit = ( onValid : UseFormSubmitHandler < T > ) => ( e : FormEvent < HTMLFormElement > ) => {
83
105
e . preventDefault ( )
106
+
107
+ // Enables validation for all form fields if not enabled after form submission.
108
+ if ( Object . keys ( enableValidationOnChange ) !== Object . keys ( data ) ) {
109
+ setEnableValidationOnChange ( Object . keys ( data ) . reduce ( ( acc , key ) => ( { ...acc , [ key ] : true } ) , { } ) )
110
+ }
111
+
84
112
const validations = options ?. validations
85
113
if ( validations ) {
86
114
const newErrors : UseFormErrors < T > = { }
@@ -149,25 +177,68 @@ export const useForm = <T extends Record<keyof T, any> = {}>(options?: {
149
177
}
150
178
151
179
/**
152
- * Registers form input fields with onChange and onBlur handlers.
180
+ * Registers form input fields with onChange, onBlur and onFocus handlers.
153
181
*
154
182
* @param name - The key of the form field to register.
155
183
* @param sanitizeFn - An optional function to sanitize the input value.
156
- * @returns An object containing ` onChange` and `onBlur ` event handlers.
184
+ * @returns An object containing form field `name`, ` onChange`, `onBlur` and `onFocus ` event handlers.
157
185
*/
158
- const register = < S extends unknown > ( name : keyof T , sanitizeFn ?: ( value : string ) => S ) => ( {
186
+ const register = < V extends unknown = string , S extends unknown = unknown > (
187
+ name : keyof T ,
188
+ sanitizeFn ?: ( value : V ) => S ,
189
+ registerOptions ?: {
190
+ /**
191
+ * Prevents the input value from being trimmed.
192
+ *
193
+ * If `noTrim` is set to true, the input value will not be automatically trimmed.\
194
+ * This can be useful when whitespace is required for certain inputs.
195
+ *
196
+ * @default false - By default, the input will be trimmed.
197
+ */
198
+ noTrim ?: boolean
199
+ } ,
200
+ ) => ( {
159
201
onChange : onChange ( name , sanitizeFn ) ,
160
- onBlur : onBlur ( name ) ,
202
+ onBlur : onBlur ( name , registerOptions ?. noTrim ) ,
203
+ onFocus : onFocus ( name ) ,
204
+ name,
161
205
} )
162
206
163
207
return {
208
+ /** The current form data. */
164
209
data,
210
+ /** An object containing validation errors for each form field. */
165
211
errors,
212
+ /**
213
+ * Registers form input fields with onChange, onBlur and onFocus handlers.
214
+ *
215
+ * @param name - The key of the form field to register.
216
+ * @param sanitizeFn - An optional function to sanitize the input value.
217
+ * @returns An object containing form field `name`, `onChange`, `onBlur` and `onFocus` event handlers.
218
+ */
166
219
register,
220
+ /**
221
+ * Handles form submission, validates all form fields, and calls the provided `onValid` function if valid.
222
+ *
223
+ * @param onValid - A function to handle valid form data on submission.
224
+ * @returns The event handler for form submission.
225
+ */
167
226
handleSubmit,
227
+ /**
228
+ * Manually triggers validation for specific form fields.
229
+ *
230
+ * @param name - The key(s) of the form field(s) to validate.
231
+ * @returns The validation error(s), if any.
232
+ */
168
233
trigger,
169
- touchedFields,
170
- dirtyFields,
171
- isDirty : Object . values ( dirtyFields ) . some ( ( value ) => value ) ,
234
+ /** An object representing additional form state. */
235
+ formState : {
236
+ /** An object indicating which fields have been touched (interacted with). */
237
+ touchedFields,
238
+ /** An object indicating which fields have been modified. */
239
+ dirtyFields,
240
+ /** A boolean indicating if any field has been modified. */
241
+ isDirty : Object . values ( dirtyFields ) . some ( ( value ) => value ) ,
242
+ } ,
172
243
}
173
244
}
0 commit comments