skip to content

With VeeValidate 4's useForm and defineField, how do you bind a sign-up form's email input and show its validation error?

level: juniorimportance: must knowfreq 34%

answer

  1. one form context per component
  2. a tuple from one call
  3. v-model plus v-bind
  4. errors keyed by path
  5. older binders are deprecated

basics

~10 s

Call useForm with a schema, then const [email, emailAttrs] = defineField('email'). Bind the input with v-model="email" and v-bind="emailAttrs", and render errors.email, the field's first error message.

solid answer

~40 s

`useForm()` turns the component into a form: it collects values, runs the `validationSchema` and aggregates errors and meta. `defineField('email')` returns a tuple: a writable model ref for `v-model`, and an attrs object with `onBlur`, `onChange` and `onInput` handlers for `v-bind`, which let VeeValidate mark the field touched and validate on blur. The first error for the field is `errors.email`; `errorBag.email` holds all of them. `defineField` is the current binding API in VeeValidate 4; `defineInputBinds`, `defineComponentBinds` and `useFieldModel` are deprecated in its favour. By default the model validates on every update, so the error appears while typing unless you configure the field otherwise.

code

vue · 26 lines
vue
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const { defineField, errors, handleSubmit } = useForm({
  validationSchema: toTypedSchema(
    z.object({ email: z.string().email('Enter a valid email') }),
  ),
  initialValues: { email: '' },
})

const [email, emailAttrs] = defineField('email')

const onSubmit = handleSubmit((values) => {
  console.log(values.email)
})
</script>

<template>
  <form novalidate @submit="onSubmit">
    <input v-model="email" v-bind="emailAttrs" type="email" />
    <p v-if="errors.email">{{ errors.email }}</p>
    <button>Sign up</button>
  </form>
</template>

go deeper

for a junior

Recall the shape: useForm once, defineField per input returning a model and attrs, v-model plus v-bind, and errors.email for the message.

for a middle

Explain what the attrs carry and why blur matters, the difference between errors and errorBag, and what form meta aggregates.

for a senior

Show you know defineField replaced the deprecated binders, that it validates on every model update by default, and how to map field state into a UI kit's props.

for a principal

Discuss standardising one binding style across a codebase, so every form reads errors, touched state and submission the same way.

## Two ways into VeeValidate VeeValidate 4 offers the same engine through two surfaces. The **composition API** (`useForm`, `defineField`, `useField`, `useFieldArray`) is what the docs recommend for most work, because it fits any input element or UI library. The **component API** (`<Form>`, `<Field>`, `<ErrorMessage>`, `<FieldArray>`) wraps those same composables for template-first code. A form built with one can host fields built with the other. ## Declaring the form and binding a field `useForm()` creates a **form context** in the current component and provides it to children. It collects field values, validates them with the `validationSchema` you pass, and aggregates errors plus `touched`, `dirty`, `valid` and `pending` state. Call it once per component. ```ts // <script setup lang="ts"> of SignUpForm.vue import { useForm } from 'vee-validate' import { toTypedSchema } from '@vee-validate/zod' import { z } from 'zod' const { defineField, errors } = useForm({ validationSchema: toTypedSchema( z.object({ email: z.string().email('Enter a valid email') }), ), initialValues: { email: '' }, }) const [email, emailAttrs] = defineField('email') ``` In the template, `<input v-model="email" v-bind="emailAttrs" type="email" />` wires both halves, and `<p v-if="errors.email">{{ errors.email }}</p>` shows the message. ## What defineField returns | Part | What it is | How you use it | |---|---|---| | `email` | a writable ref whose setter writes the form value | `v-model` | | `emailAttrs` | a computed object with `onBlur`, `onChange`, `onInput` and any props you map | `v-bind` | - **The model** writes through the form, so `values.email` updates; you never assign to `values` directly, because the docs treat it as read-only. - **The attrs** are how blur reaches VeeValidate: `onBlur` sets the field's `touched` flag and validates when blur validation is on, which it is by default. - **Mapped props**: `defineField('email', { props: state => ({ 'aria-invalid': state.errors.length > 0 }) })` adds attributes computed from the field's state, useful for UI kits that take an `error` prop. - **The path** can be nested (`'profile.email'`) or indexed (`'phones[0]'`); values are nested accordingly. ## Reading errors and state - `errors` maps each field path to its **first** error message, or nothing when valid. - `errorBag` maps each path to the **array** of messages, for fields that list every failed rule. - `meta` is the form-level aggregate: `touched` (some field blurred), `dirty` (some value changed), `valid`, `pending` (a validation still running) and `initialValues`. - The keys of `errors` are flat path strings, so a nested field's error is read as `errors['profile.email']`. ## Native inputs versus components `defineField` binds both, but they behave differently by default: - **A native `<input>`** emits `input`, `change` and `blur`, so the attrs' handlers fire as expected and every trigger setting has an event behind it. - **A component**, such as a UI kit's text field, may not emit those events at all. The docs therefore describe component validation as running immediately, on model updates, because that is the one channel every `v-model` component shares. - **Mapping props** is how a component receives state: many kits accept an `error` or `invalid` prop, which you fill from the field state through the `props` option rather than rendering a separate message. If a component does emit `blur`, `v-bind` of the attrs forwards `onBlur` as a listener, and touched state and blur validation work the same as on a native input. ## The deprecated binders Earlier VeeValidate 4 releases bound fields with `defineInputBinds` for native inputs, `defineComponentBinds` for components and `useFieldModel` for a bare model. All three are marked `@deprecated use defineField instead` in 4.15. They still run, but new code and answers should use `defineField`, which covers both native inputs and components. ## Mistakes juniors make 1. **Only `v-model`, no `v-bind`.** The value flows, but blur never reaches the form, so `touched` stays false and blur validation never runs. 2. **No initial value.** Fields start as `undefined`; the docs recommend `initialValues` because some schema libraries treat `undefined` differently from an empty string. 3. **Writing to `values`.** Assigning `values.email = ''` bypasses the form's API; use `setFieldValue` or `resetForm`. 4. **Expecting silence while typing.** `defineField` validates on every model update by default, so a half-typed address already shows an error; making that lazier is a configuration choice, not a bug.

  • How would you show the email error only after the user has left the field?
    Read the field's touched state and gate the message on it. With `defineField` you can map it through the `props` option, for example `props: state => ({ showError: state.touched && state.errors.length > 0 })`, or read it with the `useIsFieldTouched('email')` helper. The attrs' `onBlur` is what sets `touched`, so the `v-bind` must be present.
  • When would you use useField instead of defineField?
    When building a reusable input component. `useField(() => props.name)` inside the component returns its own `value`, `errorMessage`, `meta` and handlers, and registers with the nearest form automatically. `defineField` is for binding fields from the component that owns the form.
  • What is the component-API equivalent of this form?
    `<Form :validation-schema="schema">` around `<Field name="email" type="email" />` and `<ErrorMessage name="email" />`. The components use the same composables internally, so the validation behaviour and the error keys are the same.

saying these in an interview costs you the question

  • defineField returns the field's value and its error message.
  • v-model alone is enough for VeeValidate to know when the field was blurred.
  • defineInputBinds is the recommended way to bind inputs in VeeValidate 4.15.
  • You reset the email by assigning values.email = ''.
  • errors.email holds an array of every failed rule's message.