Reputation: 248
I'm fairly new to React, and I'm trying to build a dynamic form that includes an array of route stops using RHF + Typescript + Zod + ShadCN. I've reviewed the docs and have been able to implement simple text and []string fields, but am blocked at the section of a dynamic object array.
I followed the instructions here to add the ShadCN form components. Below is the relevant code. I'm using useFieldArray
and it makes perfect sense for string arrays, but I can't get it to work for an object array. I've shown the different things I've tried in the comments of the field. When I try {...form.register(
stops.${index}.city)}
, it correctly renders the default values, but the submit button doesn't do anything and the validation will throw undefined
or false negatives for wrong input. The other fields work just fine. I would greatly appreciate any help on this!
import "./styles.css";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFieldArray, useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "./button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "./form";
import { Input } from "./input";
const stopObj = z.object({
order: z.coerce.number(),
zip: z.string(),
city: z.string({ required_error: "This field is required." }),
state: z
.string({ required_error: "This field is required." })
.max(2, "2-letter state abbreviation"),
country: z.coerce
.string()
.max(2, "Two-letter country code (ISO 3166-1 alpha-2)"),
});
const formSchema = z.object({
firstName: z.string({ required_error: "This field is required." }),
currency: z.enum(["USD", "CAD"]),
stops: z
.array(stopObj)
.min(2, "2 stops minimum required (pickup and dropoff)"),
});
export default function App() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
firstName: "John",
currency: "USD",
stops: [
{ order: 1, city: "New York", state: "NY", zip: "02116", country: "US" },
{ order: 2, city: "Austin", state: "TX", zip: "12345", country: "US" },
],
},
});
async function onSubmit(values: z.infer<typeof formSchema>) {
console.log("onSubmit:", values);
}
const { fields } = useFieldArray({ name: "stops", control: form.control });
return (
<div className="bg-[#305645] w-full justify-center flex">
<div className="w-3/4 bg-white px-5 py-3 my-20 rounded-lg">
<Form {...form}>
<div className="text-center font-medium">
<h1 style={{ fontSize: "2rem", marginBottom: "1rem" }}>
Complete Form
</h1>
</div>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="flex space-x-4 w-full">
<div className="w-1/2">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="currency"
render={({ field }) => (
<FormItem>
<FormLabel>Currency</FormLabel>
<FormControl>
<Input placeholder="USD" {...field} />
</FormControl>
<FormDescription>USD or CAD.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* FIXME how to correctly render and update each stop object?? */}
{fields.map((field, index) => (
<FormField
control={form.control}
key={field.id}
name={`stops.${index}`}
render={({ field }) => (
<>
<FormItem>
<FormLabel>City</FormLabel>
<FormDescription />
<FormControl>
<Input
{...field.value.city}
// {...form.register(`stops.${index}.city`)}
// defaultValue={field.name}
// value={field.value.city}
// onChange={(e) => field.onChange(e.target.value)}
// onBlur={(e) => field.onBlur(e.target.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
<FormItem>
<FormLabel>State</FormLabel>
<FormDescription />
<FormControl>
<Input
{...form.register(`stops.${index}.state`)}
// defaultValue={field.name}
/>
</FormControl>
<FormMessage />
</FormItem>
</>
)}
/>
))}
<Button
type="submit"
className="bg-orange-500 font-extrabold w-full"
>
Submit
</Button>
</form>
</Form>
</div>
</div>
);
}
Here's a screenshot. The currency enum field works as expected, but the city & state ones do not. Even the behavior shown in the image inconsistent. It took me a few retries for the issue to appear again so I could screenshot. I'm also not getting console or compiler errors.
Upvotes: 1
Views: 8497
Reputation: 84
-> In this modify Code, use 'useForm' to manage the form state and 'useFieldArray' to manage dynamic array of stops and each stop object is rendered using 'fields.map' for each object and create indiviidual formField components for the city and state fields.
-> Using 'Control' prop to register input field with react hook form. and When submit the Form ,the form data using the 'handleSubmit' Function.
import { useForm, useFieldArray } from "react-hook-form";
import * as z from "zod";
import { Form, FormField, FormControl, FormLabel, Input } from "shadcn";
const stopObj = z.object({
order: z.number(),
zip: z.string(),
city: z.string(),
state: z.string().max(2),
country: z.string().max(2),
});
const formSchema = z.object({
firstName: z.string(),
currency: z.enum(["USD", "CAD"]),
stops: z.array(stopObj),
});
export default function App() {
const { control, handleSubmit, setValue } = useForm<z.infer<typeof formSchema>>({
defaultValues: {
firstName: "John",
currency: "USD",
stops: [
{ order: 1, city: "New York", state: "NY", zip: "02116", country: "US" },
{ order: 2, city: "Austin", state: "TX", zip: "12345", country: "US" },
],
}
});
const { fields } = useFieldArray({ control, name: "stops" });
const onSubmit = (data: z.infer<typeof formSchema>) => {
console.log(data);
};
return (
<div className="bg-[#305645] w-full justify-center flex">
<div className="w-3/4 bg-white px-5 py-3 my-20 rounded-lg">
<Form onSubmit={handleSubmit(onSubmit)}>
<div className="text-center font-medium">
<h1 style={{ fontSize: "2rem", marginBottom: "1rem" }}>
Complete Form
</h1>
</div>
<form className="space-y-4">
<FormField name="firstName" control={control}>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input />
</FormControl>
</FormField>
<FormField name="currency" control={control}>
<FormLabel>Currency</FormLabel>
<FormControl>
<Input />
</FormControl>
</FormField>
{fields.map((field, index) => (
<div key={field.id}>
<FormField name={`stops.${index}.city`} control={control}>
<FormLabel>City</FormLabel>
<FormControl>
<Input />
</FormControl>
</FormField>
<FormField name={`stops.${index}.state`} control={control}>
<FormLabel>State</FormLabel>
<FormControl>
<Input />
</FormControl>
</FormField>
</div>
))}
<button type="submit" className="bg-orange-500 font-extrabold w-full">
Submit
</button>
</form>
</Form>
</div>
</div>
);
}
Upvotes: 1