All checks were successful
continuous-integration/drone/push Build is passing
176 lines
4.6 KiB
TypeScript
176 lines
4.6 KiB
TypeScript
import { useCallback, useMemo, useState } from 'react';
|
|
|
|
type FieldErrors<TValues> = Partial<Record<keyof TValues, string | undefined>>;
|
|
type TouchedFields<TValues> = Partial<Record<keyof TValues, boolean>>;
|
|
|
|
type SetValuesOptions = {
|
|
validate?: boolean;
|
|
clearErrors?: boolean;
|
|
};
|
|
|
|
type SetFieldValueOptions = {
|
|
validate?: boolean;
|
|
touch?: boolean;
|
|
};
|
|
|
|
type ValidateAllOptions = {
|
|
touchAll?: boolean;
|
|
};
|
|
|
|
type UseValidatedFieldsOptions<TValues> = {
|
|
initialValues: TValues;
|
|
validate: (values: TValues) => FieldErrors<TValues>;
|
|
};
|
|
|
|
function hasErrors<TValues>(errors: FieldErrors<TValues>): boolean {
|
|
return Object.values(errors).some(Boolean);
|
|
}
|
|
|
|
function pickTouchedErrors<TValues>(
|
|
errors: FieldErrors<TValues>,
|
|
touched: TouchedFields<TValues>,
|
|
): FieldErrors<TValues> {
|
|
const next: FieldErrors<TValues> = {};
|
|
|
|
for (const key of Object.keys(errors) as Array<keyof TValues>) {
|
|
if (touched[key]) {
|
|
next[key] = errors[key];
|
|
}
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
function touchAll<TValues extends Record<string, string>>(values: TValues): TouchedFields<TValues> {
|
|
const touched: TouchedFields<TValues> = {};
|
|
|
|
for (const key of Object.keys(values) as Array<keyof TValues>) {
|
|
touched[key] = true;
|
|
}
|
|
|
|
return touched;
|
|
}
|
|
|
|
export function useValidatedFields<TValues extends Record<string, string>>({
|
|
initialValues,
|
|
validate,
|
|
}: UseValidatedFieldsOptions<TValues>) {
|
|
const [values, setValues] = useState<TValues>(initialValues);
|
|
const [allErrors, setAllErrors] = useState<FieldErrors<TValues>>(() => validate(initialValues));
|
|
const [touched, setTouched] = useState<TouchedFields<TValues>>({});
|
|
|
|
const updateValues = useCallback(
|
|
(nextValues: TValues, options: SetValuesOptions = {}) => {
|
|
const { validate: shouldValidate = false, clearErrors = false } = options;
|
|
setValues(nextValues);
|
|
|
|
if (shouldValidate || clearErrors) {
|
|
setAllErrors(validate(nextValues));
|
|
}
|
|
|
|
if (clearErrors) {
|
|
setTouched({});
|
|
}
|
|
},
|
|
[validate],
|
|
);
|
|
|
|
const setFieldValue = useCallback(
|
|
<K extends keyof TValues>(
|
|
key: K,
|
|
value: TValues[K],
|
|
options: SetFieldValueOptions = {},
|
|
) => {
|
|
const { validate: shouldValidate = true, touch = true } = options;
|
|
|
|
if (touch) {
|
|
setTouched((current) => ({
|
|
...current,
|
|
[key]: true,
|
|
}));
|
|
}
|
|
|
|
setValues((current) => {
|
|
const nextValues = {
|
|
...current,
|
|
[key]: value,
|
|
};
|
|
|
|
if (shouldValidate) {
|
|
setAllErrors(validate(nextValues));
|
|
}
|
|
|
|
return nextValues;
|
|
});
|
|
},
|
|
[validate],
|
|
);
|
|
|
|
const validateAll = useCallback(
|
|
(options: ValidateAllOptions = {}) => {
|
|
const { touchAll: shouldTouchAll = true } = options;
|
|
const nextErrors = validate(values);
|
|
|
|
setAllErrors(nextErrors);
|
|
|
|
if (shouldTouchAll) {
|
|
setTouched(touchAll(values));
|
|
}
|
|
|
|
return nextErrors;
|
|
},
|
|
[validate, values],
|
|
);
|
|
|
|
const setFieldError = useCallback(<K extends keyof TValues>(key: K, message?: string) => {
|
|
setTouched((current) => ({
|
|
...current,
|
|
[key]: true,
|
|
}));
|
|
|
|
setAllErrors((current) => ({
|
|
...current,
|
|
[key]: message,
|
|
}));
|
|
}, []);
|
|
|
|
const updateErrors = useCallback((nextErrors: FieldErrors<TValues>) => {
|
|
const nextTouched: TouchedFields<TValues> = {};
|
|
|
|
for (const key of Object.keys(nextErrors) as Array<keyof TValues>) {
|
|
if (nextErrors[key]) {
|
|
nextTouched[key] = true;
|
|
}
|
|
}
|
|
|
|
setTouched((current) => ({
|
|
...current,
|
|
...nextTouched,
|
|
}));
|
|
setAllErrors(nextErrors);
|
|
}, []);
|
|
|
|
const clearErrors = useCallback(() => {
|
|
setAllErrors(validate(values));
|
|
setTouched({});
|
|
}, [validate, values]);
|
|
|
|
const errors = useMemo(() => pickTouchedErrors(allErrors, touched), [allErrors, touched]);
|
|
|
|
const isValid = useMemo(() => {
|
|
return !hasErrors(validate(values));
|
|
}, [validate, values]);
|
|
|
|
return {
|
|
values,
|
|
errors,
|
|
isValid,
|
|
setValues: updateValues,
|
|
setFieldValue,
|
|
validateAll,
|
|
setFieldError,
|
|
setErrors: updateErrors,
|
|
clearErrors,
|
|
};
|
|
}
|