85 lines
1.9 KiB
TypeScript
85 lines
1.9 KiB
TypeScript
|
import { useState, ReactNode, PropsWithoutRef } from "react"
|
||
|
import { FormProvider, useForm, UseFormProps } from "react-hook-form"
|
||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||
|
import { z } from "zod"
|
||
|
|
||
|
export interface FormProps<S extends z.ZodType<any, any>>
|
||
|
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
|
||
|
/** All your form fields */
|
||
|
children?: ReactNode
|
||
|
/** Text to display in the submit button */
|
||
|
submitText?: string
|
||
|
schema?: S
|
||
|
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
|
||
|
initialValues?: UseFormProps<z.infer<S>>["defaultValues"]
|
||
|
}
|
||
|
|
||
|
interface OnSubmitResult {
|
||
|
FORM_ERROR?: string
|
||
|
|
||
|
[prop: string]: any
|
||
|
}
|
||
|
|
||
|
export const FORM_ERROR = "FORM_ERROR"
|
||
|
|
||
|
export function Form<S extends z.ZodType<any, any>>({
|
||
|
children,
|
||
|
submitText,
|
||
|
schema,
|
||
|
initialValues,
|
||
|
onSubmit,
|
||
|
...props
|
||
|
}: FormProps<S>) {
|
||
|
const ctx = useForm<z.infer<S>>({
|
||
|
mode: "onBlur",
|
||
|
resolver: schema ? zodResolver(schema) : undefined,
|
||
|
defaultValues: initialValues,
|
||
|
})
|
||
|
const [formError, setFormError] = useState<string | null>(null)
|
||
|
|
||
|
return (
|
||
|
<FormProvider {...ctx}>
|
||
|
<form
|
||
|
onSubmit={ctx.handleSubmit(async (values) => {
|
||
|
const result = (await onSubmit(values)) || {}
|
||
|
for (const [key, value] of Object.entries(result)) {
|
||
|
if (key === FORM_ERROR) {
|
||
|
setFormError(value)
|
||
|
} else {
|
||
|
ctx.setError(key as any, {
|
||
|
type: "submit",
|
||
|
message: value,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
})}
|
||
|
className="form"
|
||
|
{...props}
|
||
|
>
|
||
|
{/* Form fields supplied as children are rendered here */}
|
||
|
{children}
|
||
|
|
||
|
{formError && (
|
||
|
<div role="alert" style={{ color: "red" }}>
|
||
|
{formError}
|
||
|
</div>
|
||
|
)}
|
||
|
|
||
|
{submitText && (
|
||
|
<button type="submit" disabled={ctx.formState.isSubmitting}>
|
||
|
{submitText}
|
||
|
</button>
|
||
|
)}
|
||
|
|
||
|
<style global jsx>{`
|
||
|
.form > * + * {
|
||
|
margin-top: 1rem;
|
||
|
}
|
||
|
`}</style>
|
||
|
</form>
|
||
|
</FormProvider>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
export default Form
|