import { setHasResponded } from 'domains/app/slice' import { Provider } from 'domains/forms/context' import { useValidations } from 'domains/forms/hooks' import { getFormValuesByFormId } from 'domains/forms/selectors' import { deregisterForm, updateControlTouched as dispatchControlTouched, updateControlValue as dispatchControlValue, registerForm, } from 'domains/forms/slice' import { useAppDispatch, type RootState } from 'domains/store' import { FC } from 'preact/compat' import { useCallback, useEffect, useLayoutEffect, useMemo, useState, } from 'preact/hooks' import { useSelector } from 'react-redux' type FormProviderProps = { formId: string persistData?: boolean onError?: (_args: { errors: Record isSubmitted: boolean isValid: boolean }) => void onSubmit: Function validationSchema?: Record< string, { fn: Function errorText: string compareValue: string | number | null }[] > } const FormProvider: FC = ({ children, formId, persistData = false, onError, onSubmit, validationSchema = {}, ...props }) => { const dispatch = useAppDispatch() const values = useSelector((store: RootState) => getFormValuesByFormId(store, { formId }), ) const [isSubmitted, setIsSubmitted] = useState(false) const [externalErrors, setExternalErrors] = useState({}) const { isValid: validationIsValid, errors: validationErrors } = useValidations(values, validationSchema) const errors = useMemo( () => ({ ...validationErrors, ...externalErrors, }), [validationErrors, externalErrors], ) // register useLayoutEffect(() => { // register form in redux store dispatch(registerForm({ formId, persistData })) }, [formId, persistData, dispatch]) // deregister useEffect(() => { return () => { // deregister form from redux store dispatch(deregisterForm({ formId })) } }, [formId, persistData, dispatch]) const updateControlValue = useCallback( (name, value) => { dispatch(dispatchControlValue({ formId, name, value })) }, [formId, dispatch], ) const updateControlTouched = useCallback( (name, touched) => { dispatch(dispatchControlTouched({ formId, name, touched })) }, [dispatch, formId], ) // Function to manually set an error const setError = useCallback( (name, error) => { setExternalErrors((val) => { return { ...val, [name]: error, } }) }, [setExternalErrors], ) const handleSubmit = useCallback( (e) => { e.preventDefault() // If the submitter is set to being aria-disabled, block the submit action const ariaDisabled = e.submitter.getAttribute('aria-disabled') === 'true' setIsSubmitted(!ariaDisabled) if (!ariaDisabled && validationIsValid) { dispatch(setHasResponded(true)) onSubmit(values, { updateControlValue, setError }) } }, [ validationIsValid, dispatch, onSubmit, values, updateControlValue, setError, ], ) useEffect(() => { if (onError) { onError({ errors, isSubmitted, isValid: Object.keys(errors).length === 0, }) } }, [isSubmitted, errors, onError]) const contextValue = useMemo( () => ({ formId, values, errors, isValid: Object.keys(errors).length === 0, isSubmitted, handleSubmit, validationSchema, updateControlValue, updateControlTouched, }), [ formId, values, errors, isSubmitted, handleSubmit, validationSchema, updateControlValue, updateControlTouched, ], ) if (!formId) { console.error('"formId" is required.') return null } if (!onSubmit) { console.error('"onSubmit" is required.') return null } return ( {children} ) } export default FormProvider