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.

Where to next