With VeeValidate 4's useForm and defineField, how do you bind a sign-up form's email input and show its validation error?
answer
- one form context per component
- a tuple from one call
- v-model plus v-bind
- errors keyed by path
- older binders are deprecated
basics
~10 sCall 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<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
Recall the shape: useForm once, defineField per input returning a model and attrs, v-model plus v-bind, and errors.email for the message.
Explain what the attrs carry and why blur matters, the difference between errors and errorBag, and what form meta aggregates.
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.
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.