|
"use client"; |
|
/* eslint-disable @typescript-eslint/no-explicit-any -- We need to use `any` in form generics */ |
|
import { zodResolver } from "@hookform/resolvers/zod"; |
|
import { |
|
FieldValues, |
|
FormProvider, |
|
SubmitHandler, |
|
useForm, |
|
UseFormHandleSubmit, |
|
UseFormProps, |
|
UseFormReturn, |
|
useFormState, |
|
} from "react-hook-form"; |
|
import { useEffectEvent } from "use-effect-event"; |
|
import z from "zod"; |
|
import { Button } from "./ui/button"; |
|
|
|
export type UseDefinedFormParams< |
|
TFieldValues extends FieldValues = FieldValues, |
|
TContext = any, |
|
TTransformedValues = TFieldValues, |
|
> = Omit< |
|
UseFormProps<TFieldValues, TContext, TTransformedValues>, |
|
"resolver" |
|
> & { |
|
onSubmit: SubmitHandler<TTransformedValues>; |
|
}; |
|
|
|
export type UseDefinedFormReturn< |
|
TFieldValues extends FieldValues = FieldValues, |
|
TContext = any, |
|
TTransformedValues = TFieldValues, |
|
> = UseFormReturn<TFieldValues, TContext, TTransformedValues> & { |
|
onSubmit: ReturnType<UseFormHandleSubmit<TFieldValues, TTransformedValues>>; |
|
}; |
|
|
|
export type DefinedFormProps< |
|
TFieldValues extends FieldValues = FieldValues, |
|
TContext = any, |
|
TTransformedValues = TFieldValues, |
|
> = { |
|
form: UseDefinedFormReturn<TFieldValues, TContext, TTransformedValues>; |
|
} & Omit< |
|
React.ComponentProps<"form">, |
|
| "form" |
|
| "onSubmit" |
|
| "action" |
|
| "method" |
|
| "acceptCharset" |
|
| "encType" |
|
| "target" |
|
| "rel" |
|
>; |
|
|
|
/** |
|
* Define a useForm hook with less boilerplate a Zod schema for validation. |
|
* |
|
* @remarks |
|
* [API](https://react-hook-form.com/docs/useform) • [Demo](https://codesandbox.io/s/react-hook-form-get-started-ts-5ksmm) • [Video](https://www.youtube.com/watch?v=RkXv4AXXC_4) |
|
* |
|
* @param props - form configuration and validation parameters. |
|
* |
|
* @returns methods - individual functions to manage the form state. {@link UseDefinedFormReturn} |
|
* |
|
* @example |
|
* ```tsx |
|
* const useMyForm = defineForm(z.object({ |
|
* example: z.string(), |
|
* exampleRequired: z.string().min(1, "This field is required"), |
|
* }); |
|
* |
|
* function App() { |
|
* const form = useMyForm({ |
|
* onSubmit(data) { |
|
* console.log(data); |
|
* }, |
|
* }); |
|
* |
|
* console.log(form.watch("example")); |
|
* |
|
* return ( |
|
* <DefinedForm {...{ form }}> |
|
* <input defaultValue="test" {...form.register("example")} /> |
|
* <input {...form.register("exampleRequired", { required: true })} /> |
|
* {form.formState.errors.exampleRequired && <span>This field is required</span>} |
|
* <button>Submit</button> |
|
* </DefinedForm> |
|
* ); |
|
* } |
|
* ``` |
|
*/ |
|
export function defineForm<Schema extends z.ZodObject<any>>(schema: Schema) { |
|
type TFieldValues = z.input<Schema>; |
|
type TTransformedValues = z.output<Schema>; |
|
|
|
|
|
function useDefinedForm({ |
|
onSubmit, |
|
...params |
|
}: UseDefinedFormParams< |
|
TFieldValues, |
|
any, |
|
TTransformedValues |
|
>): UseDefinedFormReturn<TFieldValues, any, TTransformedValues> { |
|
const form = useForm<TFieldValues, any, TTransformedValues>({ |
|
...params, |
|
resolver: zodResolver(schema), |
|
}); |
|
|
|
const submitHandler = useEffectEvent( |
|
form.handleSubmit((values) => onSubmit(values)) |
|
); |
|
|
|
return { ...form, onSubmit: submitHandler }; |
|
} |
|
|
|
return useDefinedForm; |
|
} |
|
|
|
/** |
|
* A wrapper around react-hook-form's `FormProvider` and a native HTML form element. |
|
* Makes it easy to add a form by passing a `form` prop created from `useDefinedForm` |
|
* and any additional props to the `<form>` element. |
|
* |
|
* @remarks |
|
* [FormProvider API](https://react-hook-form.com/docs/formprovider) • [HTMLFormElement API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement) • [useForm API](https://react-hook-form.com/docs/useform) |
|
* |
|
* @see {@link defineForm} |
|
* @see {@link DefinedFormProps} |
|
* @see {@link FormProvider} |
|
*/ |
|
export function DefinedForm< |
|
TFieldValues extends FieldValues, |
|
TContext = any, |
|
TTransformedValues = TFieldValues, |
|
>({ |
|
form: { onSubmit, ...form }, |
|
children, |
|
...formProps |
|
}: DefinedFormProps<TFieldValues, TContext, TTransformedValues>) { |
|
return ( |
|
<FormProvider {...form}> |
|
<form {...formProps} onSubmit={onSubmit}> |
|
{children} |
|
</form> |
|
</FormProvider> |
|
); |
|
} |
|
|
|
/** |
|
* A submit button that integrates with react-hook-form's form state. |
|
* FIXME: You must implement some form of a loading state for your Button |
|
*/ |
|
export function SubmitButton({ |
|
children, |
|
...props |
|
}: React.ComponentProps<typeof Button>) { |
|
const { isSubmitting } = useFormState(); |
|
|
|
return ( |
|
<Button |
|
type='submit' |
|
{...props} |
|
// isLoading={isSubmitting || props.isLoading} |
|
> |
|
{children} |
|
</Button> |
|
); |
|
} |