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.
2
Answers
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.You can use
useFieldArray
from React Hook Form to build this type of form.Here’s a video that will help you.