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"
],
"code": "zod:invalid_format"
}
],
"name": [
{
"message": "At least 2 characters",
"path": [
"name"
],
"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> (it calls preventDefault for you, so .prevent is redundant).
Per-field writes
import { unset } from 'attaform'
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
form.interact() // simulate a full interaction so seeded values reveal their errors
form.interact(path) takes a path to arm just one subtree, which is how you surface a single field-array row's errors without a form-wide submit. See showing errors.
Every write path runs the same validation, dirty-tracking, and history pipeline.
Validation
form.validate() // reactive status ref
await form.parse({ commit: true }) // awaited verdict, lands on form.errors
Validators emit into form.errors on completion. The same pipeline runs inside handleSubmit before your success callback fires, so reach for these directly only when you need a check outside the submit cycle.