migration wip

This commit is contained in:
2025-12-29 18:18:48 +01:00
parent 292975299d
commit f86785bfb0
182 changed files with 30131 additions and 9321 deletions

View File

@@ -0,0 +1,275 @@
import { useState, useCallback, FormEvent } from 'react';
import { useFormValidation, ValidationRules, FormErrors } from './useFormValidation';
/**
* Hook for managing complete form state and submission
*/
export interface FormState<T extends Record<string, any>> {
values: T;
errors: FormErrors;
touched: Record<keyof T, boolean>;
isValid: boolean;
isSubmitting: boolean;
isSubmitted: boolean;
submitCount: number;
}
export interface FormOptions<T extends Record<string, any>> {
initialValues: T;
validationRules: Record<keyof T, ValidationRules>;
onSubmit: (values: T) => Promise<void> | void;
validateOnMount?: boolean;
}
export interface FormReturn<T extends Record<string, any>> extends FormState<T> {
setFieldValue: (field: keyof T, value: any) => void;
setFieldError: (field: keyof T, error: string) => void;
clearFieldError: (field: keyof T) => void;
handleChange: (field: keyof T, value: any) => void;
handleSubmit: (e: FormEvent<HTMLFormElement>) => Promise<void>;
reset: () => void;
setAllTouched: () => void;
setValues: (values: T) => void;
setErrors: (errors: FormErrors) => void;
setSubmitting: (isSubmitting: boolean) => void;
getFormProps: () => { onSubmit: (e: FormEvent<HTMLFormElement>) => Promise<void>; noValidate: boolean };
}
/**
* Hook for managing complete form state with validation and submission
*/
export function useForm<T extends Record<string, any>>(
options: FormOptions<T>
): FormReturn<T> {
const {
initialValues,
validationRules,
onSubmit,
validateOnMount = false,
} = options;
const {
values,
errors,
touched,
isValid,
setFieldValue: validationSetFieldValue,
setFieldError: validationSetFieldError,
clearFieldError: validationClearFieldError,
validate,
reset: validationReset,
setAllTouched: validationSetAllTouched,
setValues: validationSetValues,
} = useFormValidation<T>(initialValues, validationRules);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [submitCount, setSubmitCount] = useState(0);
// Validate on mount if requested
// Note: This is handled by useFormValidation's useEffect
const setFieldValue = useCallback((field: keyof T, value: any) => {
validationSetFieldValue(field, value);
}, [validationSetFieldValue]);
const setFieldError = useCallback((field: keyof T, error: string) => {
validationSetFieldError(field, error);
}, [validationSetFieldError]);
const clearFieldError = useCallback((field: keyof T) => {
validationClearFieldError(field);
}, [validationClearFieldError]);
const handleChange = useCallback((field: keyof T, value: any) => {
setFieldValue(field, value);
}, [setFieldValue]);
const setErrors = useCallback((newErrors: FormErrors) => {
Object.entries(newErrors).forEach(([field, fieldErrors]) => {
if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
fieldErrors.forEach((error) => {
setFieldError(field as keyof T, error);
});
}
});
}, [setFieldError]);
const setSubmitting = useCallback((state: boolean) => {
setIsSubmitting(state);
}, []);
const reset = useCallback(() => {
validationReset();
setIsSubmitting(false);
setIsSubmitted(false);
setSubmitCount(0);
}, [validationReset]);
const setAllTouched = useCallback(() => {
validationSetAllTouched();
}, [validationSetAllTouched]);
const setValues = useCallback((newValues: T) => {
validationSetValues(newValues);
}, [validationSetValues]);
const handleSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
// Increment submit count
setSubmitCount((prev) => prev + 1);
// Set all fields as touched to show validation errors
setAllTouched();
// Validate form
const validation = validate();
if (!validation.isValid) {
return;
}
// Start submission
setIsSubmitting(true);
try {
// Call submit handler
await onSubmit(values);
setIsSubmitted(true);
} catch (error) {
// Handle submission error
console.error('Form submission error:', error);
// You can set a general error or handle specific error cases
if (error instanceof Error) {
setFieldError('submit' as keyof T, error.message);
} else {
setFieldError('submit' as keyof T, 'An error occurred during submission');
}
} finally {
setIsSubmitting(false);
}
}, [values, onSubmit, validate, setAllTouched, setFieldError]);
const getFormProps = useCallback(() => ({
onSubmit: handleSubmit,
noValidate: true,
}), [handleSubmit]);
return {
values,
errors,
touched,
isValid,
isSubmitting,
isSubmitted,
submitCount,
setFieldValue,
setFieldError,
clearFieldError,
handleChange,
handleSubmit,
reset,
setAllTouched,
setValues,
setErrors,
setSubmitting,
getFormProps,
};
}
/**
* Hook for managing form state with additional utilities
*/
export function useFormWithHelpers<T extends Record<string, any>>(
options: FormOptions<T>
) {
const form = useForm<T>(options);
const getFormProps = () => ({
onSubmit: form.handleSubmit,
noValidate: true, // We handle validation manually
});
const getSubmitButtonProps = () => ({
type: 'submit',
disabled: form.isSubmitting || !form.isValid,
loading: form.isSubmitting,
});
const getResetButtonProps = () => ({
type: 'button',
onClick: form.reset,
disabled: form.isSubmitting,
});
const getFieldProps = (field: keyof T) => ({
value: form.values[field] as any,
onChange: (e: any) => {
const target = e.target;
let value: any = target.value;
if (target.type === 'checkbox') {
value = target.checked;
} else if (target.type === 'number') {
value = target.value === '' ? '' : Number(target.value);
}
form.setFieldValue(field, value);
},
error: form.errors[field as string]?.[0],
touched: form.touched[field],
onBlur: () => {
// Mark as touched on blur if not already
if (!form.touched[field]) {
form.setAllTouched();
}
},
});
const hasFieldError = (field: keyof T): boolean => {
return !!form.errors[field as string]?.length && !!form.touched[field];
};
const getFieldError = (field: keyof T): string | null => {
const errors = form.errors[field as string];
return errors && errors.length > 0 ? errors[0] : null;
};
const clearFieldError = (field: keyof T) => {
form.clearFieldError(field);
};
const setFieldError = (field: keyof T, error: string) => {
form.setFieldError(field, error);
};
const isDirty = (): boolean => {
return Object.keys(form.values).some((key) => {
const currentValue = form.values[key as keyof T];
const initialValue = options.initialValues[key as keyof T];
return currentValue !== initialValue;
});
};
const canSubmit = (): boolean => {
return !form.isSubmitting && form.isValid && isDirty();
};
return {
...form,
getFormProps,
getSubmitButtonProps,
getResetButtonProps,
getFieldProps,
hasFieldError,
getFieldError,
clearFieldError,
setFieldError,
isDirty,
canSubmit,
};
}

View File

@@ -0,0 +1,211 @@
import { useState, useCallback, ChangeEvent } from 'react';
/**
* Hook for managing individual form field state
*/
export interface FormFieldState<T> {
value: T;
error: string | null;
touched: boolean;
dirty: boolean;
isValid: boolean;
}
export interface FormFieldOptions<T> {
initialValue?: T;
validate?: (value: T) => string | null;
transform?: (value: T) => T;
}
export interface FormFieldReturn<T> {
value: T;
error: string | null;
touched: boolean;
dirty: boolean;
isValid: boolean;
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
setValue: (value: T) => void;
setError: (error: string | null) => void;
setTouched: (touched: boolean) => void;
reset: () => void;
clearError: () => void;
}
/**
* Hook for managing individual form field state with validation
*/
export function useFormField<T = string>(
options: FormFieldOptions<T> = {}
): FormFieldReturn<T> {
const {
initialValue = '' as unknown as T,
validate,
transform,
} = options;
const [state, setState] = useState<FormFieldState<T>>({
value: initialValue,
error: null,
touched: false,
dirty: false,
isValid: true,
});
const validateValue = useCallback((value: T): string | null => {
if (validate) {
return validate(value);
}
return null;
}, [validate]);
const updateState = useCallback((newState: Partial<FormFieldState<T>>) => {
setState((prev) => {
const updated = { ...prev, ...newState };
// Auto-validate if value changes and validation is provided
if ('value' in newState && validate) {
const error = validateValue(newState.value as T);
updated.error = error;
updated.isValid = !error;
}
return updated;
});
}, [validate, validateValue]);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
let value: any = e.target.value;
// Handle different input types
if (e.target.type === 'checkbox') {
value = (e.target as HTMLInputElement).checked;
} else if (e.target.type === 'number') {
value = e.target.value === '' ? '' : Number(e.target.value);
}
// Apply transformation if provided
if (transform) {
value = transform(value);
}
setState((prev) => ({
...prev,
value,
dirty: true,
touched: true,
}));
},
[transform]
);
const setValue = useCallback((value: T) => {
setState((prev) => ({
...prev,
value,
dirty: true,
touched: true,
}));
}, []);
const setError = useCallback((error: string | null) => {
setState((prev) => ({
...prev,
error,
isValid: !error,
}));
}, []);
const setTouched = useCallback((touched: boolean) => {
setState((prev) => ({
...prev,
touched,
}));
}, []);
const clearError = useCallback(() => {
setState((prev) => ({
...prev,
error: null,
isValid: true,
}));
}, []);
const reset = useCallback(() => {
setState({
value: initialValue,
error: null,
touched: false,
dirty: false,
isValid: true,
});
}, [initialValue]);
// Auto-validate on mount if initial value exists
// This ensures initial values are validated
// Note: We're intentionally not adding initialValue to dependencies
// to avoid infinite loops, but we validate once on mount
// This is handled by the updateState function when value changes
return {
value: state.value,
error: state.error,
touched: state.touched,
dirty: state.dirty,
isValid: state.isValid,
handleChange,
setValue,
setError,
setTouched,
reset,
clearError,
};
}
/**
* Hook for managing form field state with additional utilities
*/
export function useFormFieldWithHelpers<T = string>(
options: FormFieldOptions<T> & {
label?: string;
required?: boolean;
helpText?: string;
} = {}
) {
const field = useFormField<T>(options);
const hasError = field.error !== null;
const showError = field.touched && hasError;
const showSuccess = field.touched && !hasError && field.dirty;
const getAriaDescribedBy = () => {
const descriptions: string[] = [];
if (options.helpText) descriptions.push(`${options.label || 'field'}-help`);
if (field.error) descriptions.push(`${options.label || 'field'}-error`);
return descriptions.length > 0 ? descriptions.join(' ') : undefined;
};
const getInputProps = () => ({
value: field.value as any,
onChange: field.handleChange,
'aria-invalid': hasError,
'aria-describedby': getAriaDescribedBy(),
'aria-required': options.required,
});
const getLabelProps = () => ({
htmlFor: options.label?.toLowerCase().replace(/\s+/g, '-'),
required: options.required,
});
return {
...field,
hasError,
showError,
showSuccess,
getInputProps,
getLabelProps,
getAriaDescribedBy,
};
}

View File

@@ -0,0 +1,264 @@
import { useState, useEffect, useCallback } from 'react';
/**
* Form Validation Hooks
* Provides validation logic and utilities for form components
*/
export interface ValidationRule {
value: any;
message: string;
}
export interface ValidationRules {
required?: boolean | string;
minLength?: ValidationRule;
maxLength?: ValidationRule;
pattern?: ValidationRule;
min?: ValidationRule;
max?: ValidationRule;
email?: boolean | string;
url?: boolean | string;
number?: boolean | string;
custom?: (value: any) => string | null;
}
export interface ValidationError {
field: string;
message: string;
}
export interface FormErrors {
[key: string]: string[];
}
/**
* Validates a single field value against validation rules
*/
export function validateField(
value: any,
rules: ValidationRules,
fieldName: string
): string[] {
const errors: string[] = [];
// Required validation
if (rules.required) {
const requiredMessage = typeof rules.required === 'string'
? rules.required
: `${fieldName} is required`;
if (value === null || value === undefined || value === '') {
errors.push(requiredMessage);
}
}
// Only validate other rules if there's a value (unless required)
if (value === null || value === undefined || value === '') {
return errors;
}
// Min length validation
if (rules.minLength) {
const min = rules.minLength.value;
const message = rules.minLength.message || `${fieldName} must be at least ${min} characters`;
if (typeof value === 'string' && value.length < min) {
errors.push(message);
}
}
// Max length validation
if (rules.maxLength) {
const max = rules.maxLength.value;
const message = rules.maxLength.message || `${fieldName} must be at most ${max} characters`;
if (typeof value === 'string' && value.length > max) {
errors.push(message);
}
}
// Pattern validation
if (rules.pattern) {
const pattern = rules.pattern.value;
const message = rules.pattern.message || `${fieldName} format is invalid`;
if (typeof value === 'string' && !pattern.test(value)) {
errors.push(message);
}
}
// Min value validation
if (rules.min) {
const min = rules.min.value;
const message = rules.min.message || `${fieldName} must be at least ${min}`;
if (typeof value === 'number' && value < min) {
errors.push(message);
}
}
// Max value validation
if (rules.max) {
const max = rules.max.value;
const message = rules.max.message || `${fieldName} must be at most ${max}`;
if (typeof value === 'number' && value > max) {
errors.push(message);
}
}
// Email validation
if (rules.email) {
const message = typeof rules.email === 'string'
? rules.email
: 'Please enter a valid email address';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (typeof value === 'string' && !emailRegex.test(value)) {
errors.push(message);
}
}
// URL validation
if (rules.url) {
const message = typeof rules.url === 'string'
? rules.url
: 'Please enter a valid URL';
try {
new URL(value);
} catch {
errors.push(message);
}
}
// Number validation
if (rules.number) {
const message = typeof rules.number === 'string'
? rules.number
: 'Please enter a valid number';
if (isNaN(Number(value))) {
errors.push(message);
}
}
// Custom validation
if (rules.custom) {
const customError = rules.custom(value);
if (customError) {
errors.push(customError);
}
}
return errors;
}
/**
* Validates an entire form against validation rules
*/
export function validateForm<T extends Record<string, any>>(
values: T,
validationRules: Record<keyof T, ValidationRules>
): { isValid: boolean; errors: FormErrors } {
const errors: FormErrors = {};
let isValid = true;
Object.keys(validationRules).forEach((fieldName) => {
const fieldRules = validationRules[fieldName as keyof T];
const fieldValue = values[fieldName];
const fieldErrors = validateField(fieldValue, fieldRules, fieldName);
if (fieldErrors.length > 0) {
errors[fieldName] = fieldErrors;
isValid = false;
}
});
return { isValid, errors };
}
/**
* Hook for form validation
*/
export function useFormValidation<T extends Record<string, any>>(
initialValues: T,
validationRules: Record<keyof T, ValidationRules>
) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<FormErrors>({});
const [touched, setTouched] = useState<Record<keyof T, boolean>>(
Object.keys(initialValues).reduce((acc, key) => {
acc[key as keyof T] = false;
return acc;
}, {} as Record<keyof T, boolean>)
);
const [isValid, setIsValid] = useState(false);
const validate = useCallback(() => {
const validation = validateForm(values, validationRules);
setErrors(validation.errors);
setIsValid(validation.isValid);
return validation;
}, [values, validationRules]);
useEffect(() => {
validate();
}, [validate]);
const setFieldValue = (field: keyof T, value: any) => {
setValues((prev) => ({ ...prev, [field]: value }));
setTouched((prev) => ({ ...prev, [field]: true }));
};
const setFieldError = (field: keyof T, error: string) => {
setErrors((prev) => ({
...prev,
[field]: [...(prev[field as string] || []), error],
}));
};
const clearFieldError = (field: keyof T) => {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field as string];
return newErrors;
});
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched(
Object.keys(initialValues).reduce((acc, key) => {
acc[key as keyof T] = false;
return acc;
}, {} as Record<keyof T, boolean>)
);
setIsValid(false);
};
const setAllTouched = () => {
setTouched(
Object.keys(values).reduce((acc, key) => {
acc[key as keyof T] = true;
return acc;
}, {} as Record<keyof T, boolean>)
);
};
return {
values,
errors,
touched,
isValid,
setFieldValue,
setFieldError,
clearFieldError,
validate,
reset,
setAllTouched,
setValues,
};
}