import React, { createContext, useContext, useId, useRef, useState, type ComponentPropsWithoutRef, type FC, type ReactNode, } from 'react'; import { FormProvider, useForm, useFormContext, type FieldErrors, type FieldValues, type UseFormProps, } from 'react-hook-form'; import { Errors, type MaybePromise } from '@wener/utils'; import { DevOnly } from '../components/DevOnly'; import { FunctionButton } from '../components/FunctionButton'; import { showErrorToast } from '../toast'; import { cn } from '../utils/cn'; import { getDirtyFields } from './getDirtyFields'; import { getFieldErrors } from './getFieldErrors'; type ReactHookFormProviderProps = { children?: ReactNode; onSubmit?: (data: TFieldValues) => MaybePromise; } & UseFormProps; const Context = createContext(undefined); type ContextState = { id: string; // controller: UseFormReturn; onValid: (data: any) => void; onInvalid?: (errors: FieldErrors) => void; }; export namespace ReactHookForm { export let handleInvalid = _handleInvalid; export const Root: FC = (props) => { return null; }; export function Provider({ children, onSubmit, ...props }: ReactHookFormProviderProps) { const id = useId(); const controller = useForm({ mode: 'onBlur', ...props, }); const ref = useRef>({}); ref.current.onValid = onSubmit; const [ctx] = useState(() => { return { id, onValid: (data) => { ref.current.onValid?.(data); }, onInvalid: (errors) => { (ref.current.onInvalid || handleInvalid)(errors); }, }; }); return ( {children} ); } export const Form: FC> = ({ children, ...props }) => { const { handleSubmit } = useFormContext(); const { onValid, onInvalid } = Errors.BadRequest.require(useContext(Context), 'ReactHookForm: context not exists'); return (
{children}
); }; export const SubmitButton: FC< FunctionButton.SubmitButtonProps & { dirty?: boolean; } > = ({ className, dirty = true, children, ...props }) => { let context = useFormContext(); if (!context) { return null; } const { formState } = context || {}; const { isSubmitting, isDirty, disabled } = formState; let mute = disabled || isSubmitting; if (dirty) { mute ||= !isDirty; // require dirty } return ( {children} ); }; const _DebugButton = (props: FunctionButton.ButtonProps) => { const { log } = useFormDebug(); return ( DEBUG ); }; export const DebugButton: FC = (props) => { return ( <_DebugButton {...props} /> ); }; } function _handleInvalid(errors: FieldErrors) { console.error(`[FormInvalid]`, errors); const msg = getFieldErrors(errors) .map((v) => `${v.path}: ${v.error.message}`) .join('; '); showErrorToast(`表单验证失败: ${msg}`); } function useFormDebug() { const context = useFormContext(); const { formState, getValues, control } = context; return { log: () => { const { defaultValues, // name, dirtyFields, disabled, errors, isDirty, isLoading, isSubmitSuccessful, isSubmitted, isSubmitting, isValid, isValidating, submitCount, touchedFields, validatingFields, } = formState; console.log( `[FormDebug]`, 'dirty', getDirtyFields(context), 'value', getValues(), 'formState', { defaultValues, dirtyFields, disabled, errors, isDirty, isLoading, isSubmitSuccessful, isSubmitted, isSubmitting, isValid, isValidating, submitCount, touchedFields, validatingFields, }, 'props', control._options, ); }, }; }