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 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.
<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) => 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, 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.
<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, 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:
<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:
<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:
<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:
<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:
<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:
<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.
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.