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 Svelte's {#each} block. The field array provides an items array that contains unique string identifiers which the {#each} block uses to detect when an item is added, moved, or removed.
<script lang="ts">
import { createForm, Field, FieldArray, Form } from '@formisch/svelte';
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 = createForm({
schema: TodoFormSchema,
initialInput: {
heading: '',
todos: [{ label: '', deadline: '' }],
},
onSubmit: (output) => console.log(output),
});
</script>
<Form of={todoForm}>
<Field of={todoForm} path={['heading']}>
{#snippet children(field)}
<input {...field.props} value={field.input ?? ''} type="text" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
{/snippet}
</Field>
<FieldArray of={todoForm} path={['todos']}>
{#snippet children(fieldArray)}
<div>
{#each fieldArray.items as item, index (item)}
<div>
<Field of={todoForm} path={['todos', index, 'label']}>
{#snippet children(field)}
<input {...field.props} value={field.input ?? ''} type="text" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
{/snippet}
</Field>
<Field of={todoForm} path={['todos', index, 'deadline']}>
{#snippet children(field)}
<input {...field.props} value={field.input ?? ''} type="date" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
{/snippet}
</Field>
</div>
{/each}
{#if fieldArray.errors}
<div>{fieldArray.errors[0]}</div>
{/if}
</div>
{/snippet}
</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 {#each} block's context 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']}>
{#snippet children(field)}
<input {...field.props} value={field.input ?? ''} type="text" />
{/snippet}
</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:
<script lang="ts">
import { insert } from '@formisch/svelte';
const addTodo = () => {
insert(todoForm, {
path: ['todos'],
initialInput: { label: '', deadline: '' },
});
};
</script>
<button type="button" onclick={addTodo}>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:
<script lang="ts">
import { remove } from '@formisch/svelte';
const deleteTodo = (index) => {
remove(todoForm, {
path: ['todos'],
at: index,
});
};
</script>
<button type="button" onclick={() => deleteTodo(0)}>Delete</button>Move method
Move an item from one position to another:
<script lang="ts">
import { move } from '@formisch/svelte';
const moveToEnd = (items) => {
move(todoForm, {
path: ['todos'],
from: 0,
to: items.length - 1,
});
};
</script>
<button type="button" onclick={() => moveToEnd(fieldArray.items)}>
Move first to end
</button>Swap method
Swap two items in the array:
<script lang="ts">
import { swap } from '@formisch/svelte';
const swapFirst = () => {
swap(todoForm, {
path: ['todos'],
at: 0,
and: 1,
});
};
</script>
<button type="button" onclick={swapFirst}>Swap first two</button>Replace method
Replace an item with new data:
<script lang="ts">
import { replace } from '@formisch/svelte';
const replaceFirst = () => {
replace(todoForm, {
path: ['todos'],
at: 0,
initialInput: {
label: 'New task',
deadline: new Date().toISOString().split('T')[0],
},
});
};
</script>
<button type="button" onclick={replaceFirst}>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']}>
{#snippet children(categoryArray)}
{#each categoryArray.items as category, categoryIndex (category)}
<div>
<Field of={form} path={['categories', categoryIndex, 'name']}>
{#snippet children(field)}
<input {...field.props} value={field.input ?? ''} />
{/snippet}
</Field>
<FieldArray of={form} path={['categories', categoryIndex, 'items']}>
{#snippet children(itemArray)}
{#each itemArray.items as item, itemIndex (item)}
<Field
of={form}
path={[
'categories',
categoryIndex,
'items',
itemIndex,
'title',
]}
>
{#snippet children(field)}
<input {...field.props} value={field.input ?? ''} />
{/snippet}
</Field>
{/each}
{/snippet}
</FieldArray>
</div>
{/each}
{/snippet}
</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.