skip to Main Content

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):

A sketch of the form

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


  1. Chosen as BEST ANSWER

    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)
                        }}
                      />
                      
                ...
            }
        </>
      )
    

  2. You can use useFieldArray from React Hook Form to build this type of form.

    Here’s a video that will help you.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search