Files
community-rule/app/hooks/useFormValidation.ts
T
adilallo bef13261b3
CI Pipeline / test (20) (pull_request) Failing after 2m41s
CI Pipeline / test (18) (pull_request) Failing after 4m30s
CI Pipeline / e2e (firefox) (pull_request) Successful in 3m17s
CI Pipeline / e2e (webkit) (pull_request) Successful in 3m40s
CI Pipeline / e2e (chromium) (pull_request) Successful in 11m13s
CI Pipeline / visual-regression (pull_request) Successful in 6m7s
CI Pipeline / performance (pull_request) Failing after 3m40s
CI Pipeline / storybook (pull_request) Successful in 1m14s
CI Pipeline / build (pull_request) Successful in 1m37s
Fix and update tests
2026-01-26 13:16:57 -07:00

205 lines
5.2 KiB
TypeScript

import { useState, useCallback, useMemo } from "react";
/**
* Validation rule function type
*/
export type ValidationRule<T = string> = (value: T) => string | null;
/**
* Validation rules for common patterns
*/
export const validationRules = {
required: (value: unknown): string | null => {
if (value === null || value === undefined || value === "") {
return "This field is required";
}
return null;
},
email: (value: string): string | null => {
if (!value) return null; // Let required handle empty values
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value) ? null : "Please enter a valid email address";
},
minLength: (min: number) => (value: string): string | null => {
if (!value) return null;
return value.length >= min
? null
: `Must be at least ${min} characters long`;
},
maxLength: (max: number) => (value: string): string | null => {
if (!value) return null;
return value.length <= max
? null
: `Must be no more than ${max} characters long`;
},
pattern: (regex: RegExp, message: string) => (value: string): string | null => {
if (!value) return null;
return regex.test(value) ? null : message;
},
};
/**
* Form field validation state
*/
export interface FieldValidation {
value: string;
error: string | null;
touched: boolean;
dirty: boolean;
}
/**
* Options for useFormValidation hook
*/
export interface UseFormValidationOptions {
initialValues: Record<string, string>;
validationRules?: Record<string, ValidationRule[]>;
validateOnChange?: boolean;
validateOnBlur?: boolean;
}
/**
* Hook for form validation
* Provides validation state and handlers for form fields
*
* @param options - Configuration object with initial values and validation rules
*
* @returns Object with validation state, handlers, and utilities
*
* @example
* ```tsx
* const {
* values,
* errors,
* touched,
* handleChange,
* handleBlur,
* validate,
* isValid,
* } = useFormValidation({
* initialValues: { email: "", password: "" },
* validationRules: {
* email: [validationRules.required, validationRules.email],
* password: [validationRules.required, validationRules.minLength(8)],
* },
* });
*
* <input
* value={values.email}
* onChange={handleChange}
* onBlur={handleBlur}
* name="email"
* />
* {errors.email && <span>{errors.email}</span>}
* ```
*/
export function useFormValidation(options: UseFormValidationOptions) {
const {
initialValues,
validationRules: rules = {},
validateOnChange = true,
validateOnBlur = true,
} = options;
const [values, setValues] = useState<Record<string, string>>(initialValues);
const [touched, setTouched] = useState<Record<string, boolean>>({});
const [errors, setErrors] = useState<Record<string, string | null>>({});
// Validate a single field
const validateField = useCallback(
(name: string, value: string): string | null => {
const fieldRules = rules[name] || [];
for (const rule of fieldRules) {
const error = rule(value);
if (error) return error;
}
return null;
},
[rules],
);
// Validate all fields
const validate = useCallback((): boolean => {
const newErrors: Record<string, string | null> = {};
let isValid = true;
Object.keys(values).forEach((name) => {
const error = validateField(name, values[name]);
if (error) {
newErrors[name] = error;
isValid = false;
} else {
newErrors[name] = null;
}
});
setErrors(newErrors);
return isValid;
}, [values, validateField]);
// Handle field change
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setValues((prev) => ({ ...prev, [name]: value }));
if (validateOnChange) {
const error = validateField(name, value);
setErrors((prev) => ({ ...prev, [name]: error }));
}
},
[validateOnChange, validateField],
);
// Handle field blur
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name } = e.target;
setTouched((prev) => ({ ...prev, [name]: true }));
if (validateOnBlur) {
const error = validateField(name, values[name]);
setErrors((prev) => ({ ...prev, [name]: error }));
}
},
[validateOnBlur, validateField, values],
);
// Check if form is valid
const isValid = useMemo(() => {
return Object.values(errors).every((error) => error === null);
}, [errors]);
// Reset form
const reset = useCallback(() => {
setValues(initialValues);
setTouched({});
setErrors({});
}, [initialValues]);
// Set field value programmatically
const setValue = useCallback((name: string, value: string) => {
setValues((prev) => ({ ...prev, [name]: value }));
if (validateOnChange) {
const error = validateField(name, value);
setErrors((prev) => ({ ...prev, [name]: error }));
}
}, [validateOnChange, validateField]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
validate,
isValid,
reset,
setValue,
};
}