Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
75 changes: 51 additions & 24 deletions src/hook/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ import { createFormData } from "../utilities";

export type SubmitFunctionOptions = Parameters<SubmitFunction>[1];

export interface UseRemixFormOptions<T extends FieldValues>
extends UseFormProps<T> {
export interface UseRemixFormOptions<
TFieldValues extends FieldValues,
// biome-ignore lint/suspicious/noExplicitAny: defaults to any type
TContext = any,
TTransformedValues = TFieldValues,
> extends UseFormProps<TFieldValues, TContext, TTransformedValues> {
submitHandlers?: {
onValid?: SubmitHandler<T>;
onInvalid?: SubmitErrorHandler<T>;
onValid?: SubmitHandler<TTransformedValues>;
onInvalid?: SubmitErrorHandler<TFieldValues>;
};
submitConfig?: SubmitFunctionOptions;
submitData?: FieldValues;
Expand All @@ -51,23 +55,27 @@ export interface UseRemixFormOptions<T extends FieldValues>
*/
stringifyAllValues?: boolean;
}

export const useRemixForm = <T extends FieldValues>({
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<T>) => {
}: UseRemixFormOptions<TFieldValues, TContext, TTransformedValues>) => {
const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] = useState(false);
const basename = useHref("/");
const actionSubmit = useSubmit();
const actionData = useActionData();
const submit = fetcher?.submit ?? actionSubmit;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const data: any = fetcher?.data ?? actionData;
const methods = useForm<T>({ ...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(
Expand All @@ -92,7 +100,7 @@ export const useRemixForm = <T extends FieldValues>({
const onSubmit = useMemo(
() =>
(
data: T,
data: TTransformedValues,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
e: any,
formEncType?: FormEncType,
Expand Down Expand Up @@ -125,7 +133,7 @@ export const useRemixForm = <T extends FieldValues>({

// 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<T> = useMemo(
const formState: FormState<TFieldValues> = useMemo(
() => ({
get isDirty() {
return methods.formState.isDirty;
Expand Down Expand Up @@ -175,7 +183,7 @@ export const useRemixForm = <T extends FieldValues>({
const reset = useMemo(
() =>
(
values?: T | DefaultValues<T> | undefined,
values?: TFieldValues | DefaultValues<TFieldValues> | undefined,
options?: KeepStateOptions,
) => {
setIsSubmittedSuccessfully(false);
Expand All @@ -187,8 +195,8 @@ export const useRemixForm = <T extends FieldValues>({
const register = useMemo(
() =>
(
name: Path<T>,
options?: RegisterOptions<T> & {
name: Path<TFieldValues>,
options?: RegisterOptions<TFieldValues> & {
disableProgressiveEnhancement?: boolean;
},
) => {
Expand Down Expand Up @@ -241,15 +249,25 @@ export const useRemixForm = <T extends FieldValues>({

return hookReturn;
};

export type UseRemixFormReturn<T extends FieldValues> = UseFormReturn<T> & {
export type UseRemixFormReturn<
TFieldValues extends FieldValues = FieldValues,
// biome-ignore lint/suspicious/noExplicitAny: defaults to any type
TContext = any,
TTransformedValues = TFieldValues,
> = UseFormReturn<TFieldValues, TContext, TTransformedValues> & {
handleSubmit: ReturnType<typeof useRemixForm>["handleSubmit"];
reset: ReturnType<typeof useRemixForm>["reset"];
register: ReturnType<typeof useRemixForm>["register"];
};

interface RemixFormProviderProps<T extends FieldValues>
extends Omit<UseFormReturn<T>, "handleSubmit" | "reset"> {
interface RemixFormProviderProps<
TFieldValues extends FieldValues = FieldValues,
// biome-ignore lint/suspicious/noExplicitAny: defaults to any type
TContext = any,
TTransformedValues = TFieldValues,
> extends Omit<
UseFormReturn<TFieldValues, TContext, TTransformedValues>,
"handleSubmit" | "reset"
> {
children: ReactNode;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
handleSubmit: any;
Expand All @@ -258,20 +276,29 @@ interface RemixFormProviderProps<T extends FieldValues>
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
reset: any;
}
export const RemixFormProvider = <T extends FieldValues>({
export const RemixFormProvider = <
TFieldValues extends FieldValues = FieldValues,
// biome-ignore lint/suspicious/noExplicitAny: defaults to any type
TContext = any,
TTransformedValues = TFieldValues,
>({
children,
...props
}: RemixFormProviderProps<T>) => {
}: RemixFormProviderProps<TFieldValues, TContext, TTransformedValues>) => {
return <FormProvider {...props}>{children}</FormProvider>;
};

export const useRemixFormContext = <T extends FieldValues>() => {
const methods = useFormContext<T>();
export const useRemixFormContext = <
TFieldValues extends FieldValues,
// biome-ignore lint/suspicious/noExplicitAny: defaults to any type
TContext = any,
TTransformedValues = TFieldValues,
>() => {
const methods = useFormContext<TFieldValues, TContext, TTransformedValues>();
return {
...methods,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
handleSubmit: methods.handleSubmit as any as ReturnType<
UseFormHandleSubmit<T>
UseFormHandleSubmit<TFieldValues, TTransformedValues>
>,
};
};
15 changes: 12 additions & 3 deletions src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,21 @@ export function unstable_extractFormDataMiddleware({
export const getFormData = (context: unstable_RouterContextProvider) =>
context.get(formDataContext);

export const getValidatedFormData = async <T extends FieldValues>(
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<T>,
resolver: Resolver<TFieldValues, TContext, TTransformedValues>,
) => {
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 },
Expand Down
56 changes: 39 additions & 17 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,42 @@ export const getFormDataFromSearchParams = <T extends FieldValues>(
export const isGet = (request: Pick<Request, "method">) =>
request.method === "GET" || request.method === "get";

type ReturnData<T extends FieldValues> =
| { data: T; errors: undefined; receivedValues: Partial<T> }
| { data: undefined; errors: FieldErrors<T>; receivedValues: Partial<T> };
type ReturnData<
TFieldValues extends FieldValues,
TTransformedValues = TFieldValues,
> =
| {
data: TTransformedValues;
errors: undefined;
receivedValues: Partial<TFieldValues>;
}
| {
data: undefined;
errors: FieldErrors<TFieldValues>;
receivedValues: Partial<TFieldValues>;
};
/**
* 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 <T extends FieldValues>(
export const getValidatedFormData = async <
TFieldValues extends FieldValues,
// biome-ignore lint/suspicious/noExplicitAny: any by default
TContext = any,
TTransformedValues = TFieldValues,
>(
request: Request | FormData,
resolver: Resolver<T>,
resolver: Resolver<TFieldValues, TContext, TTransformedValues>,
preserveStringified = false,
): Promise<ReturnData<T>> => {
const { receivedValues } = await getFormData<T>(request, preserveStringified);
): Promise<ReturnData<TFieldValues, TTransformedValues>> => {
const { receivedValues } = await getFormData<TFieldValues>(
request,
preserveStringified,
);

const data = await validateFormData<T>(receivedValues, resolver);
const data = await validateFormData(receivedValues, resolver);

return { ...data, receivedValues };
};
Expand Down Expand Up @@ -134,24 +153,27 @@ export const getFormData = async <T extends FieldValues>(
* @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 <T extends FieldValues>(
export const validateFormData = async <
TFieldValues extends FieldValues,
TContext,
TTransformedValues = TFieldValues,
>(
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
data: any,
resolver: Resolver<T>,
resolver: Resolver<TFieldValues, TContext, TTransformedValues>,
) => {
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<T>, data: undefined };
return { errors: errors as FieldErrors<TFieldValues>, 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.
Expand Down