import { useState } from 'react'; import { usePromiseCallback, getByPath, safeToArray, type NonEmptyArray, type GetAllObjectPaths, } from '@under-control/core'; import { isGlobalValidator } from '../guards'; import type { ValidationErrorsArray, Validator } from '../types'; import { useFormValidatorsSelector, type FormValidatorsList } from './internal'; import { useFormValidatorMessages, type FormValidatorMessagesHookResult, } from './use-form-validator-messages'; type FormValidateFn = ( value: V, fields?: NonEmptyArray>, ) => Promise<{ prevErrors: ValidationErrorsArray; errors: ValidationErrorsArray; }>; export type FormValidatorHookAttrs = { validators?: FormValidatorsList; }; export type FormValidatorHookResult = { errors: FormValidatorMessagesHookResult; validating: boolean; validate: FormValidateFn; }; export function useFormValidator({ validators: validatorsGetter, }: FormValidatorHookAttrs): FormValidatorHookResult { const [errors, setErrors] = useState>([]); const validators = useFormValidatorsSelector(validatorsGetter) ?? []; const executeValidator = (value: V) => async (validator: Validator) => { // execute root level global validators if (isGlobalValidator(validator)) { const results = safeToArray( await validator({ value, }), ); return results.flatMap(item => { if (!item) { return []; } return [ { ...item, ...(item.path === undefined && { path: null, }), }, ]; }); } // execute nested validators const results = await validator.fn({ globalValue: value, value: getByPath(validator.path, value), }); return safeToArray(results).flatMap(item => { if (!item) { return []; } return [ { ...item, ...(item.path === undefined && { path: validator.path, }), }, ]; }); }; const [validate, { loading: validating }] = usePromiseCallback< FormValidateFn >(async (value, fields) => { // `usePromiseCallback` has guaranteed order of execution so // using previous state outside of `setState` is not a big issue const prevErrors = errors; // pick only specified in validate() call fields validators const filteredValidators = fields ? validators.filter( validator => // eslint-disable-next-line @typescript-eslint/no-unsafe-argument isGlobalValidator(validator) || fields.includes(validator.path), ) : validators; // execute all validators in parallel const newFieldsErrors = ( await Promise.all(filteredValidators.map(executeValidator(value))) ).flat(); const preservedErrors = fields ? prevErrors.filter(error => error.path && !fields.includes(error.path)) : []; // assign validated fields errors, if not fields provided - reset const newErrors = [...preservedErrors, ...newFieldsErrors]; if (newErrors.length || prevErrors.length) { setErrors(newErrors); } return { prevErrors, errors: newErrors, }; }); return { errors: useFormValidatorMessages({ errors }), validate, validating, }; }