record
One record, one FieldState per entry, keyed by the entry's own key. Iterate it with
v-for="(field, key) in form.record(path)"and bind:key="key".
- Category
- Return method
- Type
(path) => Readonly<Record<string, FieldState>>- Keyed
- Yes, by record key
form.record(path) is the iteration view over a record. Where list hands back an ordered array for an array path, record hands back a keyed object for a record path: one FieldState per entry, under the entry's own key. Reach for it whenever the keys are the data, set at run time rather than declared in the schema.
Iterating a record
Declare the record on your schema, then iterate form.record by its key. The keys come from the form, so you render whatever entries exist without keeping a parallel list of your own:
<script setup lang="ts">
import { useForm } from 'attaform/zod'
import { z } from 'zod'
const schema = z.object({
scoresByTeam: z.record(z.string(), z.number()),
})
const form = useForm({ schema })
</script>
<template>
<div v-for="(field, key) in form.record('scoresByTeam')" :key="key">
<label>{{ key }}</label>
<input v-register="form.register(`scoresByTeam.${key}`)" />
<p v-if="field.showErrors">{{ field.firstError?.message }}</p>
</div>
</template>
record is typed against every record path in the schema (a z.record(...), not a fixed-shape z.object({ ... })), so the path autocompletes to records only, and each entry's type narrows to the record's value shape.
Each entry is a live FieldState
Each value in the returned object is the same field state fields exposes, so every read stays live as the user types. An entry carries the full surface: field.value, field.errors, field.showErrors, field.firstError, field.touched, and the rest. Binding still flows through form.register with the entry path, which the key supplies.
Growing and shrinking
The returned object is frozen, a read-only view. A record carries its own keys, so you grow or shrink it through setValue at an entry path. Write a key that isn't there yet and a new entry joins the view:
form.setValue('scoresByTeam.west', 0)
The existing entries keep their field states and their component instances; only the new row mounts. To drop an entry, write the record back without that key.
form.fields stays the aggregate
form.fields('scoresByTeam') remains the single aggregated container for the whole record: one rolled-up FieldState whose errors, valid, and touched summarize every entry at once. Reach for the aggregate when you want one verdict for the record, and for record when you want an entry each.
list is the array counterpart
For an array, reach for list, which returns an ordered FieldState array keyed by an allocated identity token that survives reorders. record and list split cleanly by path type: a record reads through record, an array through list, and each rejects the other at compile time.
Where to next
list: the array counterpart, one FieldState per element with reorder-stable keys.- Records & maps: declaring a
z.record(...)schema and binding its entries. fields: the per-leaf FieldState every entry carries.setValue: how an entry joins or leaves the record.