Field arrays

A somewhat more special case are dynamically generated form fields based on an array. Since adding, removing, swapping and moving items can be a big challenge here, the library provides the FieldArray component which, in combination with various methods, makes it very easy for you to create such forms.

In our playground you can take a look at a form with a field array and test them out.

Create a field array

Schema definition

In the following example we create a field array for a todo form with the following schema:

import * as v from 'valibot';

const TodoFormSchema = v.object({
  heading: v.pipe(v.string(), v.nonEmpty()),
  todos: v.pipe(
    v.array(
      v.object({
        label: v.pipe(v.string(), v.nonEmpty()),
        deadline: v.pipe(v.string(), v.nonEmpty()),
      })
    ),
    v.nonEmpty(),
    v.maxLength(10)
  ),
});

FieldArray component

To dynamically generate the form fields for the todos, you use the FieldArray component in combination with Preact's Array.map(). The field array provides an items signal that contains unique string identifiers which are used to detect when an item is added, moved, or removed.

import { Field, FieldArray, Form, useForm } from '@formisch/preact';

export default function TodoPage() {
  const todoForm = useForm({
    schema: TodoFormSchema,
    initialInput: {
      heading: '',
      todos: [{ label: '', deadline: '' }],
    },
  });

  return (
    <Form of={todoForm} onSubmit={(output) => console.log(output)}>
      <Field of={todoForm} path={['heading']}>
        {(field) => (
          <>
            <input
              {...field.props}
              value={field.input.value ?? ''}
              type="text"
            />
            {field.errors.value && <div>{field.errors.value[0]}</div>}
          </>
        )}
      </Field>

      <FieldArray of={todoForm} path={['todos']}>
        {(fieldArray) => (
          <div>
            {fieldArray.items.value.map((item, index) => (
              <div key={item}>
                <Field of={todoForm} path={['todos', index, 'label']}>
                  {(field) => (
                    <>
                      <input
                        {...field.props}
                        value={field.input.value ?? ''}
                        type="text"
                      />
                      {field.errors.value && <div>{field.errors.value[0]}</div>}
                    </>
                  )}
                </Field>

                <Field of={todoForm} path={['todos', index, 'deadline']}>
                  {(field) => (
                    <>
                      <input
                        {...field.props}
                        value={field.input.value ?? ''}
                        type="date"
                      />
                      {field.errors.value && <div>{field.errors.value[0]}</div>}
                    </>
                  )}
                </Field>
              </div>
            ))}
            {fieldArray.errors.value && <div>{fieldArray.errors.value[0]}</div>}
          </div>
        )}
      </FieldArray>

      <button type="submit">Submit</button>
    </Form>
  );
}

Path array with index

As with nested fields, you use an array for the path property. The key difference is that you include the index from the map() function to specify which array item you're referencing. This ensures the paths update correctly when items are added, moved, or removed.

<Field of={todoForm} path={['todos', index, 'label']}>
  {(field) => (
    <input {...field.props} value={field.input.value ?? ''} type="text" />
  )}
</Field>

Use array methods

Now you can use the insert, move, remove, replace, and swap methods to make changes to the field array. These methods automatically take care of rearranging all the fields.

Insert method

Add a new item to the array:

import { insert } from '@formisch/preact';

<button
  type="button"
  onClick={() =>
    insert(todoForm, {
      path: ['todos'],
      initialInput: { label: '', deadline: '' },
    })
  }
>
  Add Todo
</button>;

The at option can be used to specify the index where the item should be inserted. If not provided, the item is added to the end of the array.

Remove method

Remove an item from the array:

import { remove } from '@formisch/preact';

<button
  type="button"
  onClick={() =>
    remove(todoForm, {
      path: ['todos'],
      at: index,
    })
  }
>
  Delete
</button>;

Move method

Move an item from one position to another:

import { move } from '@formisch/preact';

<button
  type="button"
  onClick={() =>
    move(todoForm, {
      path: ['todos'],
      from: 0,
      to: fieldArray.items.value.length - 1,
    })
  }
>
  Move first to end
</button>;

Swap method

Swap two items in the array:

import { swap } from '@formisch/preact';

<button
  type="button"
  onClick={() =>
    swap(todoForm, {
      path: ['todos'],
      at: 0,
      and: 1,
    })
  }
>
  Swap first two
</button>;

Replace method

Replace an item with new data:

import { replace } from '@formisch/preact';

<button
  type="button"
  onClick={() =>
    replace(todoForm, {
      path: ['todos'],
      at: 0,
      initialInput: {
        label: 'New task',
        deadline: new Date().toISOString().split('T')[0],
      },
    })
  }
>
  Replace first
</button>;

Nested field arrays

If you need to nest multiple field arrays, the path array syntax makes it straightforward. Simply extend the path with additional array indices:

const NestedSchema = v.object({
  categories: v.array(
    v.object({
      name: v.string(),
      items: v.array(
        v.object({
          title: v.string(),
        })
      ),
    })
  ),
});

<FieldArray of={form} path={['categories']}>
  {(categoryArray) => (
    <>
      {categoryArray.items.value.map((categoryItem, categoryIndex) => (
        <div key={categoryItem}>
          <Field of={form} path={['categories', categoryIndex, 'name']}>
            {(field) => (
              <input {...field.props} value={field.input.value ?? ''} />
            )}
          </Field>

          <FieldArray of={form} path={['categories', categoryIndex, 'items']}>
            {(itemArray) => (
              <>
                {itemArray.items.value.map((item, itemIndex) => (
                  <Field
                    key={item}
                    of={form}
                    path={[
                      'categories',
                      categoryIndex,
                      'items',
                      itemIndex,
                      'title',
                    ]}
                  >
                    {(field) => (
                      <input {...field.props} value={field.input.value ?? ''} />
                    )}
                  </Field>
                ))}
              </>
            )}
          </FieldArray>
        </div>
      ))}
    </>
  )}
</FieldArray>;

You can nest field arrays as deeply as you like. You will also find a suitable example of this in our playground.

Field array validation

As with fields, you can validate field arrays using Valibot's array validation functions. For example, to limit the length of the array:

const TodoFormSchema = v.object({
  todos: v.pipe(
    v.array(
      v.object({
        label: v.string(),
        deadline: v.string(),
      })
    ),
    v.minLength(1),
    v.maxLength(10)
  ),
});

The validation errors for the field array itself are available in fieldArray.errors and can be displayed alongside the array.

Contributors

Thanks to all the contributors who helped make this page better!

  • GitHub profile picture of @fabian-hiller

Partners

Thanks to our partners who support the project ideally and financially.

Sponsors

Thanks to our GitHub sponsors who support the project financially.

  • GitHub profile picture of @vasilii-kovalev
  • GitHub profile picture of @saturnonearth
  • GitHub profile picture of @ruiaraujo012
  • GitHub profile picture of @hyunbinseo
  • GitHub profile picture of @nickytonline
  • GitHub profile picture of @KubaJastrz
  • GitHub profile picture of @andrewmd5
  • GitHub profile picture of @Thanaen
  • GitHub profile picture of @caegdeveloper
  • GitHub profile picture of @bmoyroud
  • GitHub profile picture of @dslatkin