diff --git a/package.json b/package.json index e0ac03e..8fcafa2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "remix-hook-form", - "version": "7.0.0", + "version": "7.0.1", "description": "Utility wrapper around react-hook-form for use with react-router v7+", "type": "module", "main": "./dist/index.cjs", diff --git a/src/hook/index.tsx b/src/hook/index.tsx index 2f48997..bc5a91a 100644 --- a/src/hook/index.tsx +++ b/src/hook/index.tsx @@ -37,11 +37,15 @@ import { createFormData } from "../utilities"; export type SubmitFunctionOptions = Parameters[1]; -export interface UseRemixFormOptions - extends UseFormProps { +export interface UseRemixFormOptions< + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +> extends UseFormProps { submitHandlers?: { - onValid?: SubmitHandler; - onInvalid?: SubmitErrorHandler; + onValid?: SubmitHandler; + onInvalid?: SubmitErrorHandler; }; submitConfig?: SubmitFunctionOptions; submitData?: FieldValues; @@ -51,15 +55,19 @@ export interface UseRemixFormOptions */ stringifyAllValues?: boolean; } - -export const useRemixForm = ({ +export const useRemixForm = < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>({ submitHandlers, submitConfig, submitData, fetcher, stringifyAllValues = true, ...formProps -}: UseRemixFormOptions) => { +}: UseRemixFormOptions) => { const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] = useState(false); const basename = useHref("/"); const actionSubmit = useSubmit(); @@ -67,7 +75,7 @@ export const useRemixForm = ({ const submit = fetcher?.submit ?? actionSubmit; // biome-ignore lint/suspicious/noExplicitAny: const data: any = fetcher?.data ?? actionData; - const methods = useForm({ ...formProps, errors: data?.errors }); + const methods = useForm({ ...formProps, errors: data?.errors }); const navigation = useNavigation(); // Either it's submitted to an action or submitted to a fetcher (or neither) const isSubmittingForm = useMemo( @@ -92,7 +100,7 @@ export const useRemixForm = ({ const onSubmit = useMemo( () => ( - data: T, + data: TTransformedValues, // biome-ignore lint/suspicious/noExplicitAny: e: any, formEncType?: FormEncType, @@ -125,7 +133,7 @@ export const useRemixForm = ({ // React-hook-form uses lazy property getters to avoid re-rendering when properties // that aren't being used change. Using getters here preservers that lazy behavior. - const formState: FormState = useMemo( + const formState: FormState = useMemo( () => ({ get isDirty() { return methods.formState.isDirty; @@ -175,7 +183,7 @@ export const useRemixForm = ({ const reset = useMemo( () => ( - values?: T | DefaultValues | undefined, + values?: TFieldValues | DefaultValues | undefined, options?: KeepStateOptions, ) => { setIsSubmittedSuccessfully(false); @@ -187,8 +195,8 @@ export const useRemixForm = ({ const register = useMemo( () => ( - name: Path, - options?: RegisterOptions & { + name: Path, + options?: RegisterOptions & { disableProgressiveEnhancement?: boolean; }, ) => { @@ -241,15 +249,25 @@ export const useRemixForm = ({ return hookReturn; }; - -export type UseRemixFormReturn = UseFormReturn & { +export type UseRemixFormReturn< + TFieldValues extends FieldValues = FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +> = UseFormReturn & { handleSubmit: ReturnType["handleSubmit"]; reset: ReturnType["reset"]; register: ReturnType["register"]; }; - -interface RemixFormProviderProps - extends Omit, "handleSubmit" | "reset"> { +interface RemixFormProviderProps< + TFieldValues extends FieldValues = FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +> extends Omit< + UseFormReturn, + "handleSubmit" | "reset" + > { children: ReactNode; // biome-ignore lint/suspicious/noExplicitAny: handleSubmit: any; @@ -258,20 +276,29 @@ interface RemixFormProviderProps // biome-ignore lint/suspicious/noExplicitAny: reset: any; } -export const RemixFormProvider = ({ +export const RemixFormProvider = < + TFieldValues extends FieldValues = FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>({ children, ...props -}: RemixFormProviderProps) => { +}: RemixFormProviderProps) => { return {children}; }; - -export const useRemixFormContext = () => { - const methods = useFormContext(); +export const useRemixFormContext = < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>() => { + const methods = useFormContext(); return { ...methods, // biome-ignore lint/suspicious/noExplicitAny: handleSubmit: methods.handleSubmit as any as ReturnType< - UseFormHandleSubmit + UseFormHandleSubmit >, }; }; diff --git a/src/middleware/index.ts b/src/middleware/index.ts index fef00dd..fa349d8 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -36,12 +36,21 @@ export function unstable_extractFormDataMiddleware({ export const getFormData = (context: unstable_RouterContextProvider) => context.get(formDataContext); -export const getValidatedFormData = async ( +export const getValidatedFormData = async < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: defaults to any type + TContext = any, + TTransformedValues = TFieldValues, +>( context: unstable_RouterContextProvider, - resolver: Resolver, + resolver: Resolver, ) => { const formData = context.get(formDataContext); - const data = await validateFormData(formData, resolver); + const data = await validateFormData< + TFieldValues, + TContext, + TTransformedValues + >(formData, resolver); /* if (errors) { throw dataFn( { errors, receivedValues: formData }, diff --git a/src/utilities/index.ts b/src/utilities/index.ts index a8f523f..486f71c 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -90,23 +90,42 @@ export const getFormDataFromSearchParams = ( export const isGet = (request: Pick) => request.method === "GET" || request.method === "get"; -type ReturnData = - | { data: T; errors: undefined; receivedValues: Partial } - | { data: undefined; errors: FieldErrors; receivedValues: Partial }; +type ReturnData< + TFieldValues extends FieldValues, + TTransformedValues = TFieldValues, +> = + | { + data: TTransformedValues; + errors: undefined; + receivedValues: Partial; + } + | { + data: undefined; + errors: FieldErrors; + receivedValues: Partial; + }; /** * Parses the data from an HTTP request and validates it against a schema. Works in both loaders and actions, in loaders it extracts the data from the search params. * In actions it extracts it from request formData. * * @returns A Promise that resolves to an object containing the validated data or any errors that occurred during validation. */ -export const getValidatedFormData = async ( +export const getValidatedFormData = async < + TFieldValues extends FieldValues, + // biome-ignore lint/suspicious/noExplicitAny: any by default + TContext = any, + TTransformedValues = TFieldValues, +>( request: Request | FormData, - resolver: Resolver, + resolver: Resolver, preserveStringified = false, -): Promise> => { - const { receivedValues } = await getFormData(request, preserveStringified); +): Promise> => { + const { receivedValues } = await getFormData( + request, + preserveStringified, + ); - const data = await validateFormData(receivedValues, resolver); + const data = await validateFormData(receivedValues, resolver); return { ...data, receivedValues }; }; @@ -134,24 +153,27 @@ export const getFormData = async ( * @param resolver Schema to validate and cast the data with * @returns Returns the validated data if successful, otherwise returns the error object */ -export const validateFormData = async ( +export const validateFormData = async < + TFieldValues extends FieldValues, + TContext, + TTransformedValues = TFieldValues, +>( // biome-ignore lint/suspicious/noExplicitAny: data: any, - resolver: Resolver, + resolver: Resolver, ) => { const dataToValidate = data instanceof FormData ? Object.fromEntries(data) : data; - const { errors, values } = await resolver( - dataToValidate, - {}, - { shouldUseNativeValidation: false, fields: {} }, - ); + const { errors, values } = await resolver(dataToValidate, {} as TContext, { + shouldUseNativeValidation: false, + fields: {}, + }); if (Object.keys(errors).length > 0) { - return { errors: errors as FieldErrors, data: undefined }; + return { errors: errors as FieldErrors, data: undefined }; } - return { errors: undefined, data: values as T }; + return { errors: undefined, data: values as TTransformedValues }; }; /** Creates a new instance of FormData with the specified data and key.