- Simple (single-step) forms, and
- Multi-step forms (each step has its own schema and inputs).
- react-hook-form
- zod для проверки схемы
- Компоненты пользовательских оберток, такие как FormWrapper , Rhfinput
< /ul>
rhfinput.tsx
import {
forwardRef,
memo,
type Ref,
type RefCallback,
type RefObject,
} from "react";
import { useFormContext, type FieldValues, type Path } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
type RHFInputProps = {
name: Path;
label?: string;
placeholder?: string;
type?: string;
disabled?: boolean;
};
function mergeRefs(...refs: (Ref | undefined)[]): RefCallback {
return (value: T) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref && typeof ref === "object") {
(ref as RefObject).current = value;
}
});
};
}
// 1. Define generic component WITHOUT forwardRef:
function RHFInputInner(
{ name, label, placeholder, type = "text", disabled }: RHFInputProps,
ref: Ref
) {
const { control } = useFormContext();
return (
{
const { ref: fieldRef, ...restField } = field;
return (
{label && {label}}
);
}}
/>
);
}
const RHFInput = forwardRef(RHFInputInner) as (
props: RHFInputProps & { ref?: Ref }
) => React.ReactElement;
export default memo(RHFInput);
< /code>
formwrapper.tsx
import type { ReactNode } from "react";
import type {
DefaultValues,
FieldValues,
SubmitHandler,
UseFormProps,
UseFormReturn,
} from "react-hook-form";
import { ZodSchema, ZodType } from "zod";
export type FormContext =
UseFormReturn & {
readOnly: boolean;
};
export type FormStep = {
schema: ZodType;
content: React.ReactNode;
};
export interface FormWrapperProps {
isMultiStep?: boolean;
mode?: UseFormProps["mode"];
readOnly?: boolean;
defaultValues?: DefaultValues;
children?: ReactNode;
onSubmit: SubmitHandler;
steps?: FormStep[];
submitLabel?: string;
schema: ZodSchema;
className?: string;
}
import { memo, useCallback, useState } from "react";
import { isEqual } from "lodash";
import { FormProvider, useForm, type FieldValues } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { cn } from "@/lib/utils";
import { Form } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
const FormWrapper = ({
isMultiStep,
mode = "all",
readOnly = false,
defaultValues,
onSubmit,
steps,
submitLabel = "Submit",
schema,
children,
className,
}: FormWrapperProps) => {
const [step, setStep] = useState(0);
const currentStep = isMultiStep ? steps?.[step] : undefined;
const currentSchema = isMultiStep ? steps?.[step]?.schema ?? schema : schema;
const methods = useForm({
mode,
defaultValues,
resolver: zodResolver(currentSchema),
});
const extendedForm: FormContext = {
...methods,
readOnly,
};
const handleNext = useCallback(async () => {
const valid = await methods.trigger();
if (!valid) return;
setStep((prev) => Math.min(prev + 1, (steps?.length ?? 1) - 1));
}, [methods, steps]);
const handlePrev = useCallback(() => {
setStep((prev) => Math.max(prev - 1, 0));
}, []);
const handleFinalSubmit = useCallback(
(data: T) => {
onSubmit(data);
},
[onSubmit]
);
const isFirstStep = step === 0;
const isLastStep = step === (steps?.length ?? 1) - 1;
const canGoNext = !isLastStep;
const canGoPrev = !isFirstStep;
return (
{isMultiStep ? currentStep?.content : children}
{isMultiStep ? (
className={cn(
"flex items-center w-full",
canGoPrev ? "justify-between" : "justify-end"
)}
>
{canGoPrev && (
Prev
)}
{canGoNext ? (
Next
) : (
{submitLabel}
)}
) : (
{submitLabel}
)}
);
};
export default memo(FormWrapper, isEqual);
< /code>
userpage.tsx
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import FormWrapper from "@/components/form/form-wrapper";
import RHFInput from "@/components/form/controller/RHFInput";
import { z } from "zod";
import type { FormStep } from "@/components/form/types";
const step1Schema = z.object({
name: z.string().min(1, "Name is required"),
});
const step2Schema = z.object({
email: z.string().email("Invalid email"),
});
const mergedSchema = step1Schema.merge(step2Schema);
type FormType = z.infer
const steps: FormStep[] = [
{
schema: step1Schema,
content: (
),
},
{
schema: step2Schema,
content: (
),
},
];
const UserPage = () => {
return (
Create Plan
Create New Plan
console.log("Submitted data", data)}
isMultiStep
steps={steps}
>
);
};
export default UserPage;
Подробнее здесь: https://stackoverflow.com/questions/796 ... to-preserv