list

One array, one FieldState per element, in index order. Each entry carries a stable key, so a keyed v-for keeps 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.

form.list Demo Open in playground
  1. k0
  2. k1
  3. k2

Type into a row, then move it. The row.key chip and the text travel together: the key follows the athlete, not the slot.

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 / insert to add a row.
  • remove to drop one, move / swap to reorder.
  • replace to 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 the key every entry carries.
  • The v-register directive: the binding each row's input flows through.
  • errors: the per-path errors behind row.firstError.