Your first schema
Any Zod schema becomes a typed, reactive Attaform.
- Read time
- ~5 minutes
- Builds on
- Quick start
Type into any of the demo's four inputs (email, password, displayName, age) and watch the live form.values JSON below the form update in real time. form.values is the reactive view of the form data that useForm returns, paths and types straight from the schema you handed in. The What Attaform reads section below traces each Zod construct in the demo to its form behavior.
What Attaform reads
useForm is Attaform's entry point. Hand it a Zod schema and it returns a reactive form carrying every helper a template needs: a per-input binding factory, a per-field state map, and the live parsed values. Save the return value and reach for the pieces by name:
import { useForm } from 'attaform'
import { z } from 'zod'
const schema = z.object({
email: z.email(),
password: z.string().min(8),
displayName: z.string().min(2).optional(),
age: z.number().int().min(13),
})
const form = useForm({ schema })
Object fields become reactive paths on form.values; nested objects become nested paths; refinements become per-field validators surfaced through form.fields.
The schema above covers most of what a real signup form needs:
emailandpasswordare required strings. Attaform stores''as the default, soform.values.emailstarts as''and updates as the user types.displayNameis optional, and that changes what gets stored: nothing.form.values.displayNamereadsundefineduntil the user types, which is what lets an untouched optional field pass at submit. It is not holding an empty string that.optional()waves through;z.string().min(2).optional()rejects''like any other too-short string.ageis a required number. Storage starts at0, the slim default, and Attaform marks the field blank so that0is not read as a number the user chose.form.fields.age.errorscarries both themin(13)failure and a "no value supplied" entry until they enter one.
Defaults from the schema
You don't redeclare defaults when you call useForm. Attaform reads them from the schema: '' for strings, 0 for numbers, false for booleans, [] for arrays, {} for objects. An optional leaf is the exception and gets no seed at all, since the whole point of the slot is that it can be empty. The schema contract has the table for every kind, and Optional, nullable, defaulted covers what each wrapper does to it. Override per field with defaultValues:
const form = useForm({
schema,
defaultValues: {
age: 18,
displayName: 'Anonymous',
},
})
Overrides are partial. Fields you don't mention pick up the schema's own default.
Where to next
- From schema to inputs: bind the schema to native inputs with
register+v-register. - The form: every property
useFormreturns.