The form
Every form on the page is reactive and supports reading, mutations, validation, and submission handling, all typed against your schema.
- Category
- Return shape
- Returned by
useForm
One call, one form. useForm hands back a reactive form that already knows your schema's shape, types, defaults, and validation. Everything below is a property of that single form handle: drillable value reads, per-leaf field state, error arrays, the submit handler, mutators. Wire one input, then reach for whatever else you need on the same form.
const schema = z.object({
email: z.email(),
age: z.number(),
})
const form = useForm({ schema })
The demo below renders one form against an email + name schema. Type, blur, submit, reset; the panels on the right read the same form you wire into the inputs.
form.values
{
"email": "",
"name": ""
}form.errors
{
"email": [
{
"message": "Enter a valid email",
"path": [
"email"
],
"formKey": "docs-demo-the-form",
"code": "zod:invalid_format"
}
],
"name": [
{
"message": "At least 2 characters",
"path": [
"name"
],
"formKey": "docs-demo-the-form",
"code": "zod:too_small"
}
]
}form.meta
{
"dirty": false,
"valid": false,
"errorCount": 2,
"submitting": false,
"submissionAttempts": 0,
"submitted": false
}Reactive reads
form.values // drillable proxy: form.values.email
form.fields // per-leaf FieldState: form.fields.email.touched
form.errors // per-path errors: form.errors.email
form.meta // submit / valid / pending aggregates
Every read inside a reactive scope (template, computed, watchEffect) is tracked. Vue re-runs the consumer when the underlying storage changes.
Directive surface
form.register('email')
register returns the RegisterValue the v-register directive consumes. Hand it to any native input: text, number, select, checkbox, radio, textarea, file.
Submission
const onSubmit = form.handleSubmit(async (values) => {
await api.signup(values)
})
handleSubmit gates dispatch on validation. Returns a handler ready for <form @submit.prevent>.
Per-field writes
import { unset } from 'attaform/zod'
form.setValue('email', 'new@example.com')
form.setValue('age', unset) // flag any path blank by passing the sentinel
form.resetField('email')
Form-level operations
form.reset() // re-seed every path from defaultValues
form.clear() // wipe every path to its falsy-for-type baseline
Every write path runs the same validation, dirty-tracking, and history pipeline.
Validation
form.validate() // sync pass
form.validateAsync() // awaits async refinements
Validators emit into form.errors on completion. The same pipeline runs inside handleSubmit before your success callback fires, so reach for validate directly only when you need a check outside the submit cycle.