Reputation: 7583
In my form, I would like the user to have the ability to add an undetermined number of users from other platforms (in this case, the other platforms are Go (board game) servers):
My Zod schema looks like this at this point:
export const profileFormValidationSchema = z.object({
go_users: z.record(
z.string(),
z.object({
server: z.string().optional(),
username: z.string().optional(),
strength: z.string().optional(),
}),
).optional(),
})
My form is a bit complicated, but the code is open-source and you can check it out here. Here is a simplified example of what I'm doing right now:
type ProfileFormProps = {
initialValues?: ProfileFormValidation
}
export function ProfileForm({ initialValues }: ProfileFormProps) {
const profileForm = useForm<ProfileFormValidation>({
resolver: zodResolver(profileFormValidationSchema),
defaultValues: initialValues,
})
const [totalUsers, setTotalUsers] = useState(
Object.values(initialValues?.go_users ?? {}).length,
)
return (
<>
<h2 className="mt-6">Edite Seu Perfil</h2>
<Form {...profileForm}>
<form
onSubmit={profileForm.handleSubmit(onSubmit)}
className="flex flex-col gap-6"
>
<fieldset className="grid grid-cols-12 gap-x-2 gap-y-3 items-end">
<legend className="ml-3 mb-2 text-lg font-bold col-span-2">
3. Usuários em Servidores de Go
</legend>
{Array.from(Array(totalUsers + 1), (e, i) => {
const key = `user${i}`
return (
<>
<FormItem className="col-span-3">
<FormLabel className="ml-3">
Servidor - {i}
</FormLabel>
<FormControl>
<Input
placeholder="OGS"
value={
profileForm.getValues(
"go_users",
)?.[key]?.server ?? ""
}
onChange={(e) => {
const currentUsers =
profileForm.getValues(
"go_users",
)
const newGoUsers = {
...currentUsers,
}
newGoUsers[key] = {
...currentUsers?.user1,
server: e.target.value,
}
profileForm.setValue(
"go_users",
newGoUsers,
)
}}
/>
</FormControl>
<FormMessage />
</FormItem>
<FormItem className="col-span-5">
<FormLabel className="ml-3">
Nome
</FormLabel>
<FormControl>
<Input placeholder="usuário" />
</FormControl>
<FormMessage />
</FormItem>
<FormItem className="col-span-3">
<FormLabel className="ml-3">
Força
</FormLabel>
<FormControl>
<Input placeholder="10k" />
</FormControl>
<FormMessage />
</FormItem>
{i === totalUsers && (
<Button
className="col-span-1"
onClick={() =>
setTotalUsers(totalUsers + 1)
}
>
<Plus className="h-4 w-4" />
</Button>
)}
</>
)
})}
</fieldset>
<div className="w-full flex justify-end">
<Button className="w-max" type="submit">
Salvar
</Button>
</div>
</form>
</Form>
</>
)
}
The input is not really controllable like this, and the result seems a bit unpredictable.
For nested objects, I was able to control things with Zod and React Hook Form in a much cleaner way than I expected, like this:
export const profileFormValidationSchema = z.object({
socials_links: z
.record(z.string(), z.string().url().optional())
.optional(),
})
...
<FormField
control={profileForm.control}
name="socials_links.facebook"
render={({ field }) => {
return (
<FormItem className="col-span-6">
<FormLabel className="ml-3">
Facebook
</FormLabel>
<FormControl>
<Input
type="url"
placeholder="https://facebook.com/joao.silva"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)
}}
/>
But for creatable fields, I have no idea if there's a simple or clean way to do it. At any rate, those specific creatable fields don't have any fancy validation to them, so having them "manually" changed through form.setValue(...)
should be enough, I believe.
Upvotes: 0
Views: 211
Reputation: 51
You can use useFieldArray
from React Hook Form to build this type of form.
Here's a video that will help you.
You can integrate Zod by creating a schema of array of objects. Here's a working example.
const schema = z.object({
go_users: z.array(
z.object({
server: z.string(),
username: z.string(),
strength: z.string(),
})
),
});
export default function App() {
const { control, register, handleSubmit } = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: {
go_users: [{ server: "", username: "", strength: "" }],
},
});
const { fields, append } = useFieldArray({
control,
name: "go_users",
});
return (
<div className="App">
<form
onSubmit={handleSubmit((data) => {
console.log(data);
})}
>
{fields.map((field, index) => (
<div key={index}>
<input
placeholder="server"
{...register(`go_users.${index}.server`)}
/>
<input
placeholder="username"
{...register(`go_users.${index}.username`)}
/>
<input
placeholder="strength"
{...register(`go_users.${index}.strength`)}
/>
<br />
</div>
))}
<button
type="button"
onClick={() => {
append({ server: "", username: "", strength: "" });
}}
>
Add item
</button>
<button type="submit">Submit</button>
</form>
</div>
);
}
Upvotes: 1
Reputation: 7583
As a workaround, I ended up creating a local state variable for keeping track of the inputs, and then using form.setValue(...)
to sync React Hook Form to it.
export function ProfileForm({
initialValues,
}: ProfileFormProps) {
const profileForm = useForm<ProfileFormValidation>({
resolver: zodResolver(profileFormValidationSchema),
defaultValues: initialValues,
})
const [goUsers, setGoUsers] = useState<GoUsers>(
initialValues?.go_users ?? {},
)
function totalUsers() {
return Object.keys(goUsers ?? {}).length
}
return (
<>
{Array.from(Array(totalUsers() + 1), (e, i) => {
const key = `user-${i}`
return (
<div
key={i}
className="grid grid-cols-12 gap-x-2 gap-y-3 items-end"
>
<FormItem className="col-span-3">
<FormLabel className="ml-3">
Servidor
</FormLabel>
<FormControl>
<Input
placeholder="OGS"
value={goUsers?.[key]?.server ?? ""}
onChange={(e) => {
const currentUsers =
profileForm.getValues(
"go_users",
)
const newGoUsers = {
...currentUsers,
}
newGoUsers[key] = {
...currentUsers?.[key],
server: e.target.value,
}
profileForm.setValue(
"go_users",
newGoUsers,
)
setGoUsers(newGoUsers)
}}
/>
...
}
</>
)
Upvotes: 0