Check out our talk at Remix Conf!

Array of objects

In this example, we use custom inputs to manage an array of objects.

const schema = z.object({
  title: z.string().min(1),
  contacts: z
    .array(z.object({ name: z.string().min(1), email: z.string().email() }))
    .min(1),
})

const mutation = makeDomainFunction(schema)(async (values) => values)

export const action: ActionFunction = async ({ request }) =>
  formAction({ request, schema, mutation })

export default () => {
  const nameRef = useRef<HTMLInputElement>(null)
  const emailRef = useRef<HTMLInputElement>(null)

  return (
    <Form schema={schema} values={{ contacts: [] }}>
      {({ Field, Errors, Button, watch, setValue }) => {
        const contacts = watch('contacts')

        return (
          <>
            <Field name="title" />
            <Field name="contacts">
              {({ Label, Errors }) => (
                <>
                  <Label />
                  <fieldset className="flex gap-2">
                    <input
                      type="text"
                      className="block w-full rounded-md border-gray-300 text-gray-800
                      shadow-sm focus:border-pink-500 focus:ring-pink-500 sm:text-sm"
                      placeholder="Name"
                      ref={nameRef}
                    />
                    <input
                      type="text"
                      className="block w-full rounded-md border-gray-300 text-gray-800
                      shadow-sm focus:border-pink-500 focus:ring-pink-500 sm:text-sm"
                      placeholder="E-mail"
                      ref={emailRef}
                    />
                    <button
                      className="rounded-md bg-pink-500 px-4"
                      onClick={(event) => {
                        event.preventDefault()

                        const name = nameRef.current?.value
                        const email = emailRef.current?.value

                        if (name && email) {
                          setValue(
                            'contacts',
                            uniq([...contacts, { name, email }]),
                            { shouldValidate: true },
                          )
                          nameRef.current.value = ''
                          emailRef.current.value = ''
                        }
                      }}
                    >
                      +
                    </button>
                  </fieldset>
                  {contacts && (
                    <section className="-ml-1 flex flex-wrap pt-1">
                      {contacts.map((contact, index) => (
                        <span key={contact.email}>
                          <span className="m-1 flex items-center rounded-md bg-pink-500 px-2 py-1 text-white">
                            <span className="flex-1">
                              {contact.name} ({contact.email})
                            </span>
                            <button
                              className="ml-2 text-pink-700"
                              onClick={() => {
                                setValue(
                                  'contacts',
                                  contacts.filter(
                                    ({ email }) => email !== contact.email,
                                  ),
                                  { shouldValidate: true },
                                )
                              }}
                            >
                              X
                            </button>
                          </span>
                          <input
                            type="hidden"
                            name={`contacts[${index}][name]`}
                            value={contact.name}
                          />
                          <input
                            type="hidden"
                            name={`contacts[${index}][email]`}
                            value={contact.email}
                          />
                        </span>
                      ))}
                    </section>
                  )}
                  <Errors />
                </>
              )}
            </Field>
            <Errors />
            <Button />
          </>
        )
      }}
    </Form>
  )
}