2021-07-31 15:57:43 +00:00
|
|
|
import { useState, ReactNode, PropsWithoutRef } from "react";
|
|
|
|
import { FormProvider, useForm, UseFormProps } from "react-hook-form";
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
import { z } from "zod";
|
2021-07-31 14:33:18 +00:00
|
|
|
|
|
|
|
export interface FormProps<S extends z.ZodType<any, any>>
|
|
|
|
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
|
|
|
|
/** All your form fields */
|
2021-07-31 15:57:43 +00:00
|
|
|
children?: ReactNode;
|
2021-07-31 14:33:18 +00:00
|
|
|
/** Text to display in the submit button */
|
2021-07-31 15:57:43 +00:00
|
|
|
submitText?: string;
|
|
|
|
schema?: S;
|
|
|
|
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>;
|
|
|
|
initialValues?: UseFormProps<z.infer<S>>["defaultValues"];
|
2021-07-31 14:33:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface OnSubmitResult {
|
2021-07-31 15:57:43 +00:00
|
|
|
FORM_ERROR?: string;
|
2021-07-31 14:33:18 +00:00
|
|
|
|
2021-07-31 15:57:43 +00:00
|
|
|
[prop: string]: any;
|
2021-07-31 14:33:18 +00:00
|
|
|
}
|
|
|
|
|
2021-07-31 15:57:43 +00:00
|
|
|
export const FORM_ERROR = "FORM_ERROR";
|
2021-07-31 14:33:18 +00:00
|
|
|
|
|
|
|
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,
|
2021-07-31 15:57:43 +00:00
|
|
|
});
|
|
|
|
const [formError, setFormError] = useState<string | null>(null);
|
2021-07-31 14:33:18 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<FormProvider {...ctx}>
|
|
|
|
<form
|
|
|
|
onSubmit={ctx.handleSubmit(async (values) => {
|
2021-07-31 15:57:43 +00:00
|
|
|
const result = (await onSubmit(values)) || {};
|
2021-07-31 14:33:18 +00:00
|
|
|
for (const [key, value] of Object.entries(result)) {
|
|
|
|
if (key === FORM_ERROR) {
|
2021-07-31 15:57:43 +00:00
|
|
|
setFormError(value);
|
2021-07-31 14:33:18 +00:00
|
|
|
} else {
|
|
|
|
ctx.setError(key as any, {
|
|
|
|
type: "submit",
|
|
|
|
message: value,
|
2021-07-31 15:57:43 +00:00
|
|
|
});
|
2021-07-31 14:33:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})}
|
|
|
|
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>
|
2021-07-31 15:57:43 +00:00
|
|
|
);
|
2021-07-31 14:33:18 +00:00
|
|
|
}
|
|
|
|
|
2021-07-31 15:57:43 +00:00
|
|
|
export default Form;
|