list
One array, one FieldState per element, in index order. Each entry carries a stable
key, so a keyedv-forkeeps every row attached to its element through any reorder.
- Category
- Return method
- Type
(path) => readonly FieldState[]- Keyed
- Yes, by element identity
form.list(path) is the iteration view over an array. It hands back one FieldState per element, in index order, and each entry carries a key that follows its element across every shape change. Bind that key to your v-for and Vue keeps each row's component instance, input focus, and cursor attached to the element the user is working on, even after a drag-reorder.
Iterating an array
Reach for list wherever you render a repeating field. Pair it with the array index for binding and row.key for the :key:
<script setup lang="ts">
import { useForm } from 'attaform/zod'
import { z } from 'zod'
const schema = z.object({
roster: z.array(z.string()),
})
const form = useForm({ schema })
</script>
<template>
<div v-for="(row, i) in form.list('roster')" :key="row.key">
<input v-register="form.register(`roster.${i}`)" />
<p v-if="row.showErrors">{{ row.firstError?.message }}</p>
</div>
</template>
list is typed against every array path in the schema, so the path autocompletes to arrays only, and each entry's type narrows to the element shape.
Why key by row.key
row.key is an allocated identity token, not the index. It is minted once for an element and travels with it through insert, remove, move, and swap, staying distinct even when two elements hold identical values. Keying a v-for by the index instead ties each row to a slot, so a reorder reshuffles which DOM node and component instance render which element; a half-typed input can jump to the wrong row. Keying by row.key ties each row to its element, so the row a user is editing stays put when the list around it moves.
The same token is on every FieldState as field.key, reachable through form.fields('roster.0').key when you need it outside an iteration.
Each entry is a live FieldState
The entries are the same field states form.fields exposes, so every read stays live as the user interacts. A row carries the full surface: row.value, row.errors, row.showErrors, row.firstError, row.touched, and the rest.
<template>
<ul>
<li v-for="(row, i) in form.list('roster')" :key="row.key">
<input v-register="form.register(`roster.${i}`)" :aria-invalid="row.showErrors" />
<span v-if="row.showErrors" :id="row.aria.errorId">{{ row.firstError?.message }}</span>
</li>
</ul>
</template>
Binding still flows through form.register with the element path; list supplies the key and the per-row reads, and the array index supplies the register path.
form.fields stays the aggregate
form.fields('roster') remains the single aggregated container for the whole array: one rolled-up FieldState whose errors, valid, and touched summarize every element at once. That is the read for an array-level message (z.array(...).min(1) lands there). list is the complementary per-element view. Reach for the aggregate when you want one verdict for the array, and for list when you want a row each.
Read-only by design
The returned array is frozen. Identity is bookkept by the mutation helpers, so shape changes go through them rather than the view:
append/prepend/insertto add a row.removeto drop one,move/swapto reorder.replaceto overwrite a slot with a fresh element.
Each helper replays its exact change onto the identity tokens, which is what lets row.key stay true across the mutation.
record is the record counterpart
list is for arrays. For a record, whose entries are keyed rather than ordered, reach for record: it hands back a keyed object, one FieldState per entry under the entry's own key. The two split cleanly by path type, and each rejects the other at compile time.
Where to next
record: the record counterpart, one FieldState per entry keyed by the record's own key.- Field-array mutations: the seven helpers that add, remove, and reorder elements.
fields: the per-leaf FieldState and thekeyevery entry carries.- The
v-registerdirective: the binding each row's input flows through. errors: the per-path errors behindrow.firstError.