Skip to content

Commit be45323

Browse files
committed
feat: useForm - onFocus, formState, validationMode & documentation with logic refactor
1 parent e292d52 commit be45323

File tree

2 files changed

+139
-29
lines changed

2 files changed

+139
-29
lines changed

src/Shared/Hooks/useForm/useForm.ts

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,66 +13,88 @@ import {
1313
/**
1414
* A custom hook to manage form state, validation, and submission handling.
1515
*
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
2218
*/
2319
export const useForm = <T extends Record<keyof T, any> = {}>(options?: {
20+
/** An object containing validation rules for each form field. */
2421
validations?: UseFormValidations<T>
22+
/** An object representing the initial values for the form fields. */
2523
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'
2730
}) => {
2831
const [data, setData] = useState<T>((options?.initialValues || {}) as T)
2932
const [dirtyFields, setDirtyFields] = useState<DirtyFields<T>>({})
3033
const [touchedFields, setTouchedFields] = useState<TouchedFields<T>>({})
3134
const [errors, setErrors] = useState<UseFormErrors<T>>({})
35+
const [enableValidationOnChange, setEnableValidationOnChange] = useState<Partial<Record<keyof T, boolean>>>({})
3236

3337
/**
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.
3539
*
36-
* @template S - The sanitized value type.
3740
* @param key - The key of the form field to be updated.
3841
* @param sanitizeFn - An optional function to sanitize the input value.
3942
* @returns The event handler for input changes.
4043
*/
4144
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
4549
setData({
4650
...data,
4751
[key]: value,
4852
})
49-
setTouchedFields({
50-
...data,
51-
[key]: true,
52-
})
53+
const initialValues: Partial<T> = options?.initialValues ?? {}
54+
setDirtyFields({ ...dirtyFields, [key]: initialValues[key] === data[key] })
5355

54-
if (!options?.mode || options?.mode === 'onChange' || dirtyFields[key]) {
56+
const validationMode = options?.validationMode ?? 'onChange'
57+
if (validationMode === 'onChange' || enableValidationOnChange[key] || errors[key]) {
5558
const validations = options?.validations ?? {}
5659
const error = checkValidation<T>(value as T[keyof T], validations[key as string])
5760
setErrors({ ...errors, [key]: error })
5861
}
5962
}
6063

6164
/**
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'.
6366
*
64-
* @param key - The key of the form field to be validated on blur.
67+
* @param key - The key of the form field.
6568
* @returns The event handler for the blur event.
6669
*/
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') {
7076
const validations = options?.validations ?? {}
7177
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+
}
7281
setErrors({ ...errors, [key]: error })
7382
}
7483
}
7584

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+
7698
/**
7799
* Handles form submission, validates all form fields, and calls the provided `onValid` function if valid.
78100
*
@@ -81,6 +103,12 @@ export const useForm = <T extends Record<keyof T, any> = {}>(options?: {
81103
*/
82104
const handleSubmit = (onValid: UseFormSubmitHandler<T>) => (e: FormEvent<HTMLFormElement>) => {
83105
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+
84112
const validations = options?.validations
85113
if (validations) {
86114
const newErrors: UseFormErrors<T> = {}
@@ -149,25 +177,68 @@ export const useForm = <T extends Record<keyof T, any> = {}>(options?: {
149177
}
150178

151179
/**
152-
* Registers form input fields with onChange and onBlur handlers.
180+
* Registers form input fields with onChange, onBlur and onFocus handlers.
153181
*
154182
* @param name - The key of the form field to register.
155183
* @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.
157185
*/
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+
) => ({
159201
onChange: onChange(name, sanitizeFn),
160-
onBlur: onBlur(name),
202+
onBlur: onBlur(name, registerOptions?.noTrim),
203+
onFocus: onFocus(name),
204+
name,
161205
})
162206

163207
return {
208+
/** The current form data. */
164209
data,
210+
/** An object containing validation errors for each form field. */
165211
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+
*/
166219
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+
*/
167226
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+
*/
168233
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+
},
172243
}
173244
}

src/Shared/Hooks/useForm/useForm.types.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { FormEvent } from 'react'
22

3+
/**
4+
* Describes the "required" validation rule.
5+
* It can be a simple boolean or an object containing a boolean value and an error message.
6+
*/
37
type ValidationRequired =
48
| boolean
59
| {
610
value: boolean
711
message: string
812
}
913

14+
/**
15+
* Describes the "pattern" validation rule, which ensures a value matches a specific regular expression.
16+
* It can be a single validation object or an array of multiple patterns.
17+
*/
1018
type ValidationPattern =
1119
| {
1220
value: RegExp
@@ -17,6 +25,11 @@ type ValidationPattern =
1725
message: string
1826
}[]
1927

28+
/**
29+
* Describes custom validation logic.
30+
* It checks if a value passes a custom validation function, which returns a boolean.
31+
* If validation fails, an error message is provided.
32+
*/
2033
type ValidationCustom =
2134
| {
2235
isValid: (value: string) => boolean
@@ -27,18 +40,44 @@ type ValidationCustom =
2740
message: string
2841
}[]
2942

43+
/**
44+
* Defines the validation rules for form fields.
45+
* Includes `required`, `pattern`, and `custom` validation types.
46+
*/
3047
export interface UseFormValidation {
3148
required?: ValidationRequired
3249
pattern?: ValidationPattern
3350
custom?: ValidationCustom
3451
}
3552

53+
/**
54+
* Represents the structure for form validation errors.
55+
* Maps each field to an error message or an array of error messages.
56+
*/
3657
export type UseFormErrors<T> = Partial<Record<keyof T, string | string[]>>
3758

59+
/**
60+
* Represents the fields that have been modified ("dirty") in the form.
61+
* Maps each field to a boolean value indicating whether it has been changed.
62+
*/
3863
export type DirtyFields<T> = Partial<Record<keyof T, boolean>>
3964

65+
/**
66+
* Represents the fields that have been interacted with ("touched") in the form.
67+
* Maps each field to a boolean value indicating whether it has been focused or interacted with.
68+
*/
4069
export type TouchedFields<T> = Partial<Record<keyof T, boolean>>
4170

71+
/**
72+
* Defines the structure for form validations.
73+
* Maps each form field to its corresponding validation rules.
74+
*/
4275
export type UseFormValidations<T extends {}> = Partial<Record<keyof T, UseFormValidation>>
4376

77+
/**
78+
* Describes the function signature for handling form submission.
79+
*
80+
* @param data - The form data collected during submission.
81+
* @param e - The form event, optionally passed when the form is submitted.
82+
*/
4483
export type UseFormSubmitHandler<T extends {}> = (data: T, e?: FormEvent<HTMLFormElement>) => void

0 commit comments

Comments
 (0)