# Field arrays

> This document is the Markdown version of [formisch.dev/vue/guides/field-arrays/](https://formisch.dev/vue/guides/field-arrays/). For the complete documentation index, see [llms.txt](https://formisch.dev/llms.txt).

Dynamically generating form fields from an array introduces additional challenges when items are added, removed, swapped, or moved. Formisch provides the [`FieldArray`](/vue/api/FieldArray.md) component, which works with its array methods to make these forms straightforward.

> In our [playground](/playground/todos/) you can take a look at a form with a field array and test it out.

## Create a field array

### Schema definition

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

```ts
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`](/vue/api/FieldArray.md) component in combination with Vue's `v-for` directive. The field array provides an `items` array that contains unique string identifiers which the `v-for` directive uses to detect when an item is added, moved, or removed.

```vue
<script setup lang="ts">
import { Field, FieldArray, Form, useForm } from '@formisch/vue';
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)
  ),
});

const todoForm = useForm({
  schema: TodoFormSchema,
  initialInput: {
    heading: '',
    todos: [{ label: '', deadline: '' }],
  },
});
</script>

<template>
  <Form :of="todoForm" @submit="(output, event) => console.log(output)">
    <Field :of="todoForm" :path="['heading']" v-slot="field">
      <input v-bind="field.props" v-model="field.input" type="text" />
      <div v-if="field.errors">{{ field.errors[0] }}</div>
    </Field>

    <FieldArray :of="todoForm" :path="['todos']" v-slot="fieldArray">
      <div>
        <div v-for="(item, index) in fieldArray.items" :key="item">
          <Field
            :of="todoForm"
            :path="['todos', index, 'label']"
            v-slot="field"
          >
            <input v-bind="field.props" v-model="field.input" type="text" />
            <div v-if="field.errors">{{ field.errors[0] }}</div>
          </Field>

          <Field
            :of="todoForm"
            :path="['todos', index, 'deadline']"
            v-slot="field"
          >
            <input v-bind="field.props" v-model="field.input" type="date" />
            <div v-if="field.errors">{{ field.errors[0] }}</div>
          </Field>
        </div>
        <div v-if="fieldArray.errors">{{ fieldArray.errors[0] }}</div>
      </div>
    </FieldArray>

    <button type="submit">Submit</button>
  </Form>
</template>
```

### Path array with index

As with [nested fields](/vue/guides/nested-fields.md), you use an array for the `path` property. The key difference is that you include the index from the `v-for` directive to specify which array item you're referencing. This ensures the paths update correctly when items are added, moved, or removed.

```vue
<template>
  <Field :of="todoForm" :path="['todos', index, 'label']" v-slot="field">
    <input v-bind="field.props" v-model="field.input" type="text" />
  </Field>
</template>
```

## Use array methods

Now you can use the [`insert`](/methods/api/insert.md), [`move`](/methods/api/move.md), [`remove`](/methods/api/remove.md), [`replace`](/methods/api/replace.md), and [`swap`](/methods/api/swap.md) 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:

```vue
<script setup lang="ts">
import { insert } from '@formisch/vue';
</script>

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

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:

```vue
<script setup lang="ts">
import { remove } from '@formisch/vue';
</script>

<template>
  <button
    type="button"
    @click="remove(todoForm, { path: ['todos'], at: index })"
  >
    Delete
  </button>
</template>
```

### Move method

Move an item from one position to another:

```vue
<script setup lang="ts">
import { move } from '@formisch/vue';
</script>

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

### Swap method

Swap two items in the array:

```vue
<script setup lang="ts">
import { swap } from '@formisch/vue';
</script>

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

### Replace method

Replace an item with new data:

```vue
<script setup lang="ts">
import { replace } from '@formisch/vue';
</script>

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

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

```vue
<script setup lang="ts">
import * as v from 'valibot';

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

<template>
  <FieldArray :of="form" :path="['categories']" v-slot="categoryArray">
    <div
      v-for="(categoryItem, categoryIndex) in categoryArray.items"
      :key="categoryItem"
    >
      <Field
        :of="form"
        :path="['categories', categoryIndex, 'name']"
        v-slot="field"
      >
        <input v-bind="field.props" v-model="field.input" />
      </Field>

      <FieldArray
        :of="form"
        :path="['categories', categoryIndex, 'items']"
        v-slot="itemArray"
      >
        <div v-for="(item, itemIndex) in itemArray.items" :key="item">
          <Field
            :of="form"
            :path="['categories', categoryIndex, 'items', itemIndex, 'title']"
            v-slot="field"
          >
            <input v-bind="field.props" v-model="field.input" />
          </Field>
        </div>
      </FieldArray>
    </div>
  </FieldArray>
</template>
```

You can nest field arrays as deeply as you like. You will also find a suitable example of this in our [playground](/playground/nested/).

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

```ts
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.
