import type { Dispatch, SetStateAction, RefObject, } from "react"; import { useEffect, useState, useCallback, } from "react"; import { getElRefHelper } from "./getElRefHelper"; import { useListeners } from "./useListeners"; import { constants } from "../../constants"; import type { TValidationEvent as TInputValidationEvent, TValidationInput } from "../../plugins/forms/validation"; type TFormValidationEvent = CustomEvent<{ isValid: boolean; inputs: TValidationInput[]; }>; type TValidationEntity = "form" | "input"; type TValidationEventMap = { form: TFormValidationEvent; input: TInputValidationEvent; }; type TValidState = boolean | null; type TValidationResult = { isValid: TValidState; invalidInputNames: string[]; setIsValid: Dispatch>; }; /** * Добавляет валидацию на элемент формы либо на форму целиком и возвращает её результат * @template TType - тип источника валидации: `"input"` для отдельного поля или `"form"` для всей формы. * @returns {TValidationResult} Возвращает флаги валидации (`isValid`, `invalidInputNames`) и `setIsValid` для ручного сброса состояния. * @example * import { useRef } from "react"; * import { useFormValidation } from "@delement/ui/utils/react"; * import { Form, FormRow, Btn, Input } from "@delement/ui/components"; * * const App = () => { * * const ref = useRef(null); * const { isValid, invalidInputNames } = useFormValidation(ref, null, "form"); // invalidInputNames содержит массив полей с ошибками * * return ( *
* * * * * * * * * *
* ) * } */ export const useFormValidation = ( ref: RefObject, onValidate: ((event: TValidationEventMap[TType]) => void) | null = null, type: TType = "input" as TType ): TValidationResult => { const [ isValid, setIsValid ] = useState(null); const [ invalidInputNames, setInvalidInputNames ] = useState([]); // robust preventing of re-render const updateClassnames = (isValid: TValidationResult["isValid"] = null) => { const el = getElRefHelper(ref); if (el) { if (isValid === null) { // unknown validation state el.classList.remove(constants.stateClasses.invalid, constants.stateClasses.valid); } else { // validation is boolean el.classList.toggle(constants.stateClasses.invalid, !isValid); el.classList.toggle(constants.stateClasses.valid, isValid); } } }; const handleValidate = useCallback((event: TValidationEventMap[TType]) => { if (typeof onValidate === "function") { onValidate(event); } else { const el = getElRefHelper(ref); if (el) { const isValid = event.detail.isValid; // robust preventing of re-render updateClassnames(isValid); setIsValid(() => isValid); if (type === "form") { const inputs = "inputs" in event.detail ? event.detail.inputs : []; const invalidInputNames = inputs.filter((input) => input.classList.contains(constants.stateClasses.invalid)).map(({ name }) => name); setInvalidInputNames(() => invalidInputNames); } else { setInvalidInputNames(() => []); } } else { console.error("[useFormValidation] Invalid ref"); } } }, [ ref, onValidate, type ]); useListeners({ listeners: { [constants.formBubbles[type === "input" ? "inputValid" : "formValid"]]: handleValidate, [constants.formBubbles[type === "input" ? "inputInvalid" : "formInvalid"]]: handleValidate, }, ref, }); useEffect(() => { return () => { setIsValid(null); setInvalidInputNames([]); }; }, []); return { isValid, invalidInputNames, setIsValid, }; };