Not getting the desired form data with React Hook Form, Zod and Ant Design

22 views Asked by At

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>
        );
      }}
    />
  );
};
0

There are 0 answers