useFormValidator
Vue composables for form validation with Valibot - useFormValidator and useFormField provide a flexible and typed approach to handle form validation in your Vue applications.
Introduction
useFormValidator and useFormField are two Vue composables that work together to provide powerful form validation using Valibot.
- useFormValidator: Initializes form validation for your entire form. Use it in your form's parent component.
- useFormField: Manages individual field validation states. Use it when you need fine-grained control over a field or when fields are in child components.
When to use each:
| Composable | Use When |
|---|---|
useFormValidator only | Simple forms where all fields are in the same component |
useFormValidator + useFormField | Fields in child components, or when using eager, blur, or progressive validation modes |
Quick Start
Here's the simplest form you can create with useFormValidator:
Understanding Form State
useFormValidator returns several reactive values to help you manage your form:
| Property | Type | Description |
|---|---|---|
model | Ref<Model> | The form data object - bind this to your inputs with v-model |
isValid | ComputedRef<boolean> | true when all fields pass validation |
isDirty | ComputedRef<boolean> | true when any field has been modified from its initial value |
isSubmitting | Ref<boolean> | true while the form is being submitted |
isSubmitted | Ref<boolean> | true after the form has been submitted at least once |
errorMessages | ComputedRef<Record<string, string>> | The first error message for each field (if any) |
errors | ComputedRef<Record<string, ValidationIssues>> | All validation issues for each field |
fieldsStates | Ref<FieldsStates> | Detailed state object for each field |
handleSubmit | Function | Wrapper function for form submission |
validateForm | Function | Manually trigger form validation |
resetForm | Function | Reset the form to its initial state |
scrollToError | Function | Scroll to the first field with an error |
Field States
Each field in fieldsStates contains:
| Property | Type | Description |
|---|---|---|
valid | boolean | Field passes validation |
error | boolean | Field has an error that should be displayed |
errors | ValidationIssues | Array of all validation issues |
dirty | boolean | Field value differs from initial value |
blurred | boolean | Field has lost focus at least once |
validated | boolean | Validation has run at least once |
validating | boolean | Async validation is in progress |
Form State:
{
"isValid": false,
"isDirty": false,
"isSubmitted": false,
"isSubmitting": false
}Fields States:
{
"name": {
"blurred": false,
"dirty": false,
"errors": [],
"error": false,
"valid": false,
"validating": false,
"validated": false,
"mode": "lazy"
},
"age": {
"blurred": false,
"dirty": false,
"errors": [],
"error": false,
"valid": false,
"validating": false,
"validated": false,
"mode": "lazy"
}
}Validation Modes
Validation modes control when validation runs and when errors are displayed. Choose the mode that best fits your UX needs.
| Mode | Validates On | Shows Errors | Best For |
|---|---|---|---|
lazy (default) | Value change | After change (if not empty) | Simple forms |
aggressive | Immediately + every change | Always | Real-time feedback |
eager | Blur, then on change | After first blur | Better UX |
blur | Only on blur | After blur | Minimal interruption |
progressive | Silently, shows on blur if invalid | After blur or validation | Optimal UX |
TIP
For eager, blur, and progressive modes, you must use useFormField with the ref option or validationEvents to capture blur events.
Lazy Mode (Default)
The default mode. Validates when field values change. Errors only appear if the field is not empty.
Type in the field and clear it - notice the error appears only when there's content.
Aggressive Mode
Validates all fields immediately when the form is created and on every change. Errors are always displayed.
Notice all fields show errors immediately, even before any interaction.
Eager Mode (Recommended)
Validates on blur first (if the field is not empty), then on every change. This provides a good balance between immediate feedback and not overwhelming the user.
WARNING
Requires useFormField with ref option or validationEvents.
Type something, then click outside the field (blur) to see validation. After that, errors update as you type.
Blur Mode
WARNING
Requires useFormField with ref option or validationEvents.
Validates only when the field loses focus. Errors are only shown after blur.
Type in the field, then click outside. Errors only appear after blur, and don't update while typing.
Progressive Mode
WARNING
Requires useFormField with ref option or validationEvents.
The most user-friendly mode. Validates silently in the background. Shows errors only on blur if the field is invalid. Once valid, it stays valid until it becomes invalid again.
Start typing - the field becomes valid (green) as soon as validation passes. Errors only show after blur.
useFormField for Child Components
useFormField is essential when:
- Your form fields are in child components
- You need the
eager,blur, orprogressivevalidation modes - You want fine-grained control over individual field states
Return Values
| Property | Type | Description |
|---|---|---|
value | WritableComputedRef<T> | The field value (use with v-model) |
hasError | ComputedRef<boolean> | Field has an error that should be displayed |
errors | ComputedRef<ValidationIssues> | All validation issues |
errorMessage | ComputedRef<string> | First error message |
isValid | ComputedRef<boolean> | Field passes validation |
isDirty | ComputedRef<boolean> | Field has been modified |
isBlurred | ComputedRef<boolean> | Field has lost focus |
isValidated | ComputedRef<boolean> | Validation has run |
isValidating | ComputedRef<boolean> | Async validation in progress |
mode | ComputedRef<string> | The validation mode |
validationEvents | ComputedRef<object> | Blur event handler for v-bind |
Two Ways to Bind Validation Events
Option 1: Using ref (Recommended)
Pass a template ref to useFormField. It will automatically detect interactive elements and attach blur listeners.
<script setup>
import { useFormField } from 'maz-ui/composables'
import { useTemplateRef } from 'vue'
const { value, errorMessage, hasError } = useFormField<string>('email', {
ref: useTemplateRef('emailRef'),
formIdentifier: 'my-form',
})
</script>
<template>
<MazInput
ref="emailRef"
v-model="value"
:hint="errorMessage"
:error="hasError"
/>
</template>Option 2: Using validationEvents
If your component emits a blur event, you can use v-bind with validationEvents.
<script setup>
import { useFormField } from 'maz-ui/composables'
const { value, errorMessage, hasError, validationEvents } = useFormField<string>('email', {
formIdentifier: 'my-form',
})
</script>
<template>
<MazInput
v-model="value"
v-bind="validationEvents"
:hint="errorMessage"
:error="hasError"
/>
</template>TypeScript Type Inference
The form model is automatically typed based on your schema:
const schema = {
name: pipe(string(), nonEmpty()),
age: pipe(number(), minValue(0)),
email: pipe(string(), email()),
}
const { model } = useFormValidator({ schema })
// model.value is typed as: { name?: string, age?: number, email?: string }For useFormField, specify the field type as a generic parameter:
// Specify the type for better type safety
const { value } = useFormField<string>('name', { formIdentifier: 'my-form' })
// value is typed as WritableComputedRef<string>Common TypeScript Errors
If you get circular reference errors with useTemplateRef, use the classic ref() instead:
// May cause TypeScript errors
const { value: email } = useFormField<string>('email', {
ref: useTemplateRef('emailRef'),
})
// Solution 1: Add generic to useTemplateRef
const { value: email } = useFormField<string>('email', {
ref: useTemplateRef<HTMLInputElement>('emailRef'),
})
// Solution 2: Use classic ref
const emailRef = ref<HTMLInputElement>()
const { value: email } = useFormField<string>('email', {
ref: emailRef,
})Async Validation
Use Valibot's pipeAsync and checkAsync for async validations like checking username availability:
Try typing "taken" - the async validator will reject it after a 2-second delay.
Throttling and Debouncing
For expensive validations (like API calls), use throttling or debouncing to limit how often validation runs.
| Option | Behavior | Default Time | Use Case |
|---|---|---|---|
debouncedFields | Waits until user stops typing | 300ms | Search fields, API calls |
throttledFields | Runs at most once per interval | 1000ms | Rate-limited APIs |
Name has 500ms debounce, Age has 1000ms throttle. Watch the console to see validation timing.
Reset Form
Use resetForm() to reset the form to its initial state, or set resetOnSuccess to automatically reset after successful submission.
Multiple Forms on Same Page
Use the identifier option to have multiple independent forms on the same page. Make sure to match the identifier in both useFormValidator and useFormField.
<script lang="ts" setup>
import { useFormValidator, useFormField } from 'maz-ui/composables'
// Form 1
const form1 = useFormValidator({
schema: schema1,
options: { identifier: 'login-form' },
})
// Form 2
const form2 = useFormValidator({
schema: schema2,
options: { identifier: 'register-form' },
})
// useFormField must use matching identifier
const { value: loginEmail } = useFormField<string>('email', {
formIdentifier: 'login-form',
})
const { value: registerEmail } = useFormField<string>('email', {
formIdentifier: 'register-form',
})
</script>Error Handling and scrollToError
scrollToError
Automatically scroll to the first field with an error when validation fails:
const { handleSubmit } = useFormValidator({
schema,
options: {
scrollToError: '.has-error', // CSS selector for error elements
// scrollToError: false, // Disable scrolling
},
})Add the matching class to your fields:
<MazInput
:class="{ 'has-error': hasError }"
:error="hasError"
/>onError Callback
Handle validation failures with the onError callback:
const onSubmit = handleSubmit(
(data) => {
// Success callback
console.log('Valid:', data)
},
'.has-error', // scrollToError selector (optional)
{
onError: ({ model, errorMessages, errors }) => {
// Called when validation fails
console.log('Validation failed:', errorMessages)
},
}
)Performance & Best Practices
Performance Tips
- Use throttling/debouncing for expensive validations (API calls, complex logic)
- Prefer
eagerorprogressivemodes overaggressivefor better performance - Use
lazymode for simple forms with minimal validation - Avoid
aggressivemode on large forms - it validates every field on every change
Common Patterns
Multiple Forms with Identifiers
const { handleSubmit } = useFormValidator({
schema,
options: { identifier: 'my-unique-form' },
})
const { value } = useFormField<string>('email', {
formIdentifier: 'my-unique-form', // Must match!
})Custom Interactive Elements
If your custom component isn't detected for blur events, add data-interactive:
<div data-interactive class="custom-input" tabindex="0">
Custom Input
</div>Common Pitfalls
| Pitfall | Solution |
|---|---|
Mismatched formIdentifier | Ensure useFormField's formIdentifier matches useFormValidator's identifier |
| Validation not triggering in eager/blur/progressive modes | Use ref option or v-bind="validationEvents" |
TypeScript errors with useTemplateRef | Add generic type or use classic ref() |
| Field not found warning | Make sure the field name exists in your schema |
API Reference
useFormValidator
Parameters
useFormValidator<TSchema>({
schema: TSchema, // Valibot validation schema (required)
model?: Ref<Model>, // External model ref (optional)
defaultValues?: DeepPartial<Model>, // Initial values (optional)
options?: {
mode?: 'lazy' | 'aggressive' | 'eager' | 'blur' | 'progressive', // Default: 'lazy'
throttledFields?: Record<string, number | true>, // Fields to throttle
debouncedFields?: Record<string, number | true>, // Fields to debounce
scrollToError?: string | false, // CSS selector, default: '.has-field-error'
identifier?: string | symbol, // Form identifier, default: 'main-form-validator'
resetOnSuccess?: boolean, // Reset after submit, default: true
}
})Return Values
{
identifier: string | symbol
model: Ref<Model>
isValid: ComputedRef<boolean>
isDirty: ComputedRef<boolean>
isSubmitting: Ref<boolean>
isSubmitted: Ref<boolean>
errors: ComputedRef<Record<string, ValidationIssues>>
errorMessages: ComputedRef<Record<string, string | undefined>>
fieldsStates: Ref<FieldsStates<Model>>
validateForm: (setErrors?: boolean) => Promise<void[]>
scrollToError: (selector?: string) => void
resetForm: () => void
handleSubmit: <Func>(
successCallback: Func,
scrollToError?: string | false,
options?: { onError?: Function, resetOnSuccess?: boolean }
) => (event?: Event) => Promise<ReturnType<Func>>
}useFormField
Parameters
useFormField<FieldType>(
name: string, // Field name in schema (required)
options?: {
defaultValue?: FieldType, // Default value for this field
mode?: 'lazy' | 'aggressive' | 'eager' | 'blur' | 'progressive', // Override form mode
ref?: Ref<HTMLElement | ComponentInstance>, // Template ref for blur detection
formIdentifier?: string | symbol, // Must match useFormValidator's identifier
}
)Return Values
{
value: WritableComputedRef<FieldType>
hasError: ComputedRef<boolean>
errors: ComputedRef<ValidationIssues>
errorMessage: ComputedRef<string | undefined>
isValid: ComputedRef<boolean>
isDirty: ComputedRef<boolean>
isBlurred: ComputedRef<boolean>
isValidated: ComputedRef<boolean>
isValidating: ComputedRef<boolean>
mode: ComputedRef<string | undefined>
validationEvents: ComputedRef<{ onBlur?: () => void }>
}Types
interface FormValidatorOptions<Model> {
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
throttledFields?: Partial<Record<keyof Model, number | true>>
debouncedFields?: Partial<Record<keyof Model, number | true>>
scrollToError?: string | false
identifier?: string | symbol
resetOnSuccess?: boolean
}
interface FormFieldOptions<FieldType> {
defaultValue?: FieldType
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
ref?: Ref<HTMLElement | ComponentInstance>
formIdentifier?: string | symbol
}
interface FieldState<FieldType> {
valid: boolean
error: boolean
errors: ValidationIssues
dirty: boolean
blurred: boolean
validated: boolean
validating: boolean
initialValue?: Readonly<FieldType>
mode?: string
}Troubleshooting
Type Errors with useTemplateRef
Problem: TypeScript circular reference errors when using useTemplateRef
Solutions:
// Solution 1: Add generic type
const { value } = useFormField<string>('email', {
ref: useTemplateRef<HTMLInputElement>('emailRef'),
})
// Solution 2: Use classic ref
const emailRef = ref<HTMLInputElement>()
const { value } = useFormField<string>('email', { ref: emailRef })Validation Not Triggering
Problem: eager, blur, or progressive mode not validating
Solution: These modes require blur event detection. Use either:
// Option 1: ref option
const { value } = useFormField<string>('name', {
ref: useTemplateRef('inputRef'),
})
// Option 2: validationEvents
const { value, validationEvents } = useFormField<string>('name')
// Then: v-bind="validationEvents" on your inputElement Not Found Warning
Problem: No element found for ref in field 'name'
Solutions:
- Ensure the ref is bound to an HTML element or Vue component
- Make sure the component has a
$elproperty - For custom components, add
data-interactiveattribute
Mismatched Form Identifiers
Problem: useFormField not finding the form context
Solution: Ensure identifiers match:
// In parent
const { handleSubmit } = useFormValidator({
schema,
options: { identifier: 'my-form' }, // This identifier...
})
// In child
const { value } = useFormField<string>('email', {
formIdentifier: 'my-form', // ...must match this one
})