I have a form made with react-hook-form, zod and ant-design, but I'm having trouble getting the data from the form to look how I want it to.
Here's an example for how I'd like the data to look after form submission, when logged inside of the onSubmit() handler:
const formData = {
subject_travel: "yes",
transport_percentages: [
{
transport_id: 1,
percentage: 20,
},
{
transport_id: 2,
percentage: 40,
},
{
transport_id: 3,
percentage: 40,
},
],
do_you_know_avg_distance_travelled: "no",
};
Here is my form:
import { type SubmitHandler } from "react-hook-form";
import { z } from "zod";
import useCalulatorStore from "@/store/useCalulatorStore";
import Form from "./form/Form";
import { RadioGroupField } from "./form/fields/RadioGroupField";
import { Col, Row } from "antd";
import { InputNumberField } from "./form/fields/InputNumberField";
import {
invalidNumberErrorMessage,
percentageField,
} from "./form/supply/schema";
const mockData = [
{
id: 1,
name: "car",
},
{
id: 2,
name: "bike",
},
{
id: 3,
name: "plane",
},
];
const zodTransportPercentage = z.object({
transport_id: z.number().nonnegative(),
percentage: percentageField,
});
const subject_travel = z.discriminatedUnion("subject_travel", [
z.object({
subject_travel: z.literal("yes"),
transport_percentages: z.array(zodTransportPercentage),
}),
z.object({
subject_travel: z.literal("no"),
}),
]);
const do_you_know_avg_distance_travelled = z.discriminatedUnion(
"do_you_know_avg_distance_travelled",
[
z.object({
do_you_know_avg_distance_travelled: z.literal("yes"),
avg_distance: z
.number({ invalid_type_error: invalidNumberErrorMessage })
.nonnegative(),
}),
z.object({
do_you_know_avg_distance_travelled: z.literal("no"),
}),
]
);
const subject_travel_schema = subject_travel.and(
do_you_know_avg_distance_travelled
);
export type UserDataEntrySubjectTravelData = z.infer<
typeof subject_travel_schema
>;
const UserDataEntrySubjectTravel = () => {
const { setData, stepUserDataEntrySubjectTravel } = useCalulatorStore();
const onSubmit: SubmitHandler<UserDataEntrySubjectTravelData> = async (
data
) => {
setData({ step: 8, data });
console.log(data);
alert(JSON.stringify(data));
};
return (
<>
<Form<UserDataEntrySubjectTravelData>
zodSchema={subject_travel_schema}
onSubmit={onSubmit}
defaultValues={stepUserDataEntrySubjectTravel || undefined}
>
{({ watch, control }) => {
const isSubjectTravelYes = watch("subject_travel") === "yes";
const isTravelDistanceYes =
watch("do_you_know_avg_distance_travelled") === "yes";
return (
<>
<RadioGroupField
control={control}
name="subject_travel"
label="Do you have an average subject travel survey data?"
options={[
{ label: "Yes", value: "yes" },
{ label: "No", value: "no" },
]}
/>
{isSubjectTravelYes && (
<Row gutter={20}>
{mockData.map((t, i) => (
<Col>
<InputNumberField
control={control}
label={t.name}
name={`transport_percentages.${i}.percentage`}
key={t.id}
/>
</Col>
))}
</Row>
)}
<RadioGroupField
control={control}
name="do_you_know_avg_distance_travelled"
label="Do you know the average distance travelled?"
options={[
{ label: "Yes", value: "yes" },
{ label: "No", value: "no" },
]}
/>
{isTravelDistanceYes && (
<InputNumberField
control={control}
label="Distance"
name="avg_distance"
/>
)}
</>
);
}}
</Form>
</>
);
};
export default UserDataEntrySubjectTravel;
I get this error when trying to submit data, using the above example formData input values:
{
"transport_percentages": [
{
"transport_id": {
"message": "Required",
"type": "invalid_type"
}
},
{
"transport_id": {
"message": "Required",
"type": "invalid_type"
}
},
{
"transport_id": {
"message": "Required",
"type": "invalid_type"
}
}
]
}
Here's what the Form component looks like:
import type { FormProps as FormAntDProps } from "antd";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import {
FieldValues,
useForm,
UseFormReturn,
SubmitHandler,
FormProvider,
DefaultValues,
} from "react-hook-form";
import { Button, Form as FormAntD } from "antd";
type FormAntDPropsWithoutChildren = Omit<FormAntDProps, "children">;
// Form
type FormProps<TFormValues extends FieldValues> =
FormAntDPropsWithoutChildren & {
onSubmit: SubmitHandler<TFormValues>;
children: (methods: UseFormReturn<TFormValues>) => React.ReactNode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
zodSchema: z.Schema<any>;
buttonText?: string;
defaultValues?: DefaultValues<TFormValues>;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Form = <TFormValues extends Record<string, any> = Record<string, any>>({
onSubmit,
children,
zodSchema,
buttonText,
defaultValues,
...rest
}: FormProps<TFormValues>) => {
const methods = useForm<TFormValues>({
resolver: zodResolver(zodSchema),
defaultValues,
});
return (
<>
<FormProvider {...methods}>
<FormAntD
onFinish={methods.handleSubmit(onSubmit)}
labelCol={rest.labelCol ?? { span: 24 }}
>
{children(methods)}
<br />
<FormAntD.Item>
<Button type="primary" htmlType="submit">
{buttonText ?? "Submit"}
</Button>
</FormAntD.Item>
<pre>{JSON.stringify(methods.formState.errors, null, 2)}</pre>
</FormAntD>
</FormProvider>
</>
);
};
export default Form;
And here's the InputNumberField component:
import { Form, FormItemProps, InputNumber } from "antd";
import { InputNumberProps } from "antd/lib";
import { ReactNode } from "react";
import { Control, Controller, FieldPath, FieldValues } from "react-hook-form";
type InputNumberFieldProps<TFieldValues extends FieldValues = FieldValues> =
InputNumberProps & {
control: Control<TFieldValues>;
name: FieldPath<TFieldValues>;
label: ReactNode;
customHelp?: string;
formItemProps?: FormItemProps;
fullWidth?: boolean;
};
export const InputNumberField = <
TFieldValues extends FieldValues = FieldValues,
>({
name,
label,
control,
customHelp,
formItemProps,
fullWidth = true,
...props
}: InputNumberFieldProps<TFieldValues>) => {
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => {
const { onChange, value, onBlur } = field;
const { error } = fieldState;
return (
<Form.Item
{...formItemProps}
htmlFor={name}
label={label}
validateStatus={error ? "error" : "validating"}
help={error ? error?.message : customHelp || undefined}
>
<InputNumber
{...props}
id={name}
value={value}
onChange={onChange}
onBlur={onBlur}
{...(fullWidth && { style: { width: "100%" } })}
/>
</Form.Item>
);
}}
/>
);
};