useFormValidator
useFormValidator
and useFormField
are two Vue composables designed to simplify form validation using Valibot as the validation library. These composables offer a flexible and typed approach to handle form validation in your Vue applications.
Introduction
Best Practices
- Use typed Valibot schemas to ensure type consistency.
- Choose the appropriate validation mode based on your form's needs.
- Use
useFormField
for fine-grained management of each form field. - Use the
handleSubmit
returned byuseFormValidator
to handle form submission securely. - Leverage computed values like
isValid
,hasError
,errorMessage
, and others to control your user interface state.
Validation modes details
lazy
: (default) Validates only on value changesaggressive
: Validates all fields immediately and on every changeeager
: (recommended) Validates on initial blur (if not empty), then on every change (requiresuseFormField
to add validation events)blur
: Validates only on focus loss (requiresuseFormField
to add validation events)progressive
: Validates the field at each user interaction (value change or blur). The field becomes valid after the first successful validation and then validated on input value change. If the field is invalid, the error message on the first blur event (requiresuseFormField
to add validation events)
How to get TypeScript type safety?
The model is typed automatically from the schema.
import { pipe, string, nonEmpty, number, minValue, maxValue, minLength } from 'valibot'
import { useFormValidator, useFormField } from 'maz-ui/composables'
const schema = {
name: pipe(string('Name is required'), nonEmpty('Name is required'), minLength(3, 'Name must be at least 3 characters')),
age: pipe(number('Age is required'), minValue(18, 'Age must be greater than 18'), maxValue(100, 'Age must be less than 100')),
country: pipe(string('Country is required'), nonEmpty('Country is required')),
}
// Automatic type inference from schema
const { model } = useFormValidator({
schema,
})
// For useFormField, specify both schema and field name for precise typing
const { value: name } = useFormField<string>('name', { formIdentifier: 'form' })
How to bind validation events with useFormField for eager, blur, or progressive modes?
To use the eager
, blur
, or progressive
validation modes, you must use the useFormField
composable to add the necessary validation events.
2 ways to bind validation events:
1. Use the ref
attribute on the component to get the reference
You can use the ref
attribute on the component and pass the reference to the useFormField
composable.
This method will automatically detect interactive elements (input, select, textarea, button, elements with ARIA roles, etc.) within the component and add the necessary validation events.
<template>
<MazInput
ref="inputRef"
v-model="value"
:hint="errorMessage"
:error="hasError"
:success="isValid"
/>
<!-- Work with HTML input -->
<input ref="inputRef" v-model="value" />
</template>
<script setup lang="ts">
import { useFormField } from 'maz-ui/composables'
import { useTemplateRef } from 'vue'
const { value, errorMessage, isValid, hasError } = useFormField<string>('name', {
ref: useTemplateRef<HTMLInputElement>('inputRef'),
})
</script>
2. Use the v-bind
directive to bind the validation events
You can use the v-bind
directive to bind the validation events to the component or HTML element.
If you use this method with a custom component, the component must emit the blur
event to trigger the field validation. Otherwise, use the first method.
<template>
<MazInput
v-model="value"
:hint="errorMessage"
:error="hasError"
:success="isValid"
v-bind="validationEvents"
/>
<!-- or -->
<input v-model="value" v-bind="validationEvents" />
</template>
<script setup lang="ts">
import { useFormField } from 'maz-ui/composables'
const { value, errorMessage, isValid, hasError, validationEvents } = useFormField<string>('name')
</script>
Basic Usage with lazy mode
In this example, we will create a simple form with four fields: name
, age
, agree
and country
. The form will be validated in lazy
mode, which means that the fields will be validated on every change.
TIP
Submit the form to show the validation errors
{ "isValid": false, "isSubmitting": false, "isDirty": false, "isSubmitted": false, "errorMessages": {} }
Usage with useFormField
In this example, we will use the useFormField
composable to handle the validation of each field individually.
Eager mode
With eager mode, each form field is validated on blur (if not empty) and then on every change. This mode is made for a better user experience, as the user will see the validation errors only after they have finished typing.
Progressive mode
With progressive mode, the field becomes valid after the first successful validation and then validated on input value change. If the field is invalid, the error message is shown on the first blur event.
Throlling and Debouncing
You can use the throttledFields
and debouncedFields
options to throttle or debounce the validation of specific fields.
The fields are validated with throttling or debouncing to avoid spamming the server or to wait for the user to finish typing before validating.
You can set the throttle or debounce time in milliseconds or use true
for the default throttle time (1000ms) or debounce time (300ms).
Validation with async function
You can use async functions in the validation schema.
useFormValidator
useFormValidator
is the main composable for initializing form validation.
It accepts a validation schema, default values, and configuration options to handle form validation. You can also provide a model reference to bind the form data.
Parameters
useFormValidator<TSchema>
accepts an object with the following properties:
schema
:TSchema
- The Valibot validation schema for the form.model
:Ref<Model>
(optional) - A reference to the form's data model.defaultValues
:DeepPartial<Model>
(optional) - Default values for the form fields.options
:FormValidatorOptions
(optional) - Configuration options for the form validation behavior.mode
:'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
(optional) - Form validation mode. (default: 'lazy') - To use theeager
,blur
, orprogressive
validation modes, you must use theuseFormField
composable to add the necessary validation events. - see validation modesthrottledFields
:Partial<Record<ModelKey, number | true>>
(optional) - Fields to validate with throttling. It's an object where the key is the field name and the value is the throttle time in milliseconds ortrue
for the default throttle time (1000ms).debouncedFields
:Partial<Record<ModelKey, number | true>>
(optional) - Fields to validate with debouncing. It's an object where the key is the field name and the value is the debounce time in milliseconds ortrue
for the default debounce time (300ms).scrollToError
:string | false
(optional) - Disable or provide a CSS selector for scrolling to errors (default '.has-field-error')identifier
:string | symbol
(optional) - Identifier for the form (useful when you have multiple forms on the same component)
Return
useFormValidator
returns an object containing:
isDirty
:ComputedRef<boolean>
- Indicates if the form has been modified.isSubmitting
:Ref<boolean>
- Indicates if the form is currently being submitted.isSubmitted
:Ref<boolean>
- Indicates if the form has been submitted.isValid
:ComputedRef<boolean>
- Indicates if the form is valid.errors
:ComputedRef<Record<ModelKey, ValidationIssues>>
- Validation errors for each field.errorsMessages
:ComputedRef<Record<string, string>>
- The first validation error message for each field.model
:Ref<Model>
- The form's data model.fieldsStates
:FieldsStates
- The validation state of each field.validateForm
:(setErrors?: boolean) => Promise<boolean>
- Function to validate the entire form.scrollToError
:(selector?: string, options?: { offset?: number }) => void
- Function to scroll to the first field with an error.handleSubmit
:successCallback: (model: Model) => Promise<unknown> | unknown, scrollToError?: false | string
- Form submission handler, the callback is called if the form is valid and passes the complete payload as an argument. The second argument is optional and can be used to disable or provide a CSS selector for scrolling to errors (default '.has-field-error').
useFormField
WARNING
Before using useFormField
, make sure you have initialized the form with useFormValidator
.
useFormField
is a composable for handling validation at the individual form field level.
Useful for fine-grained control over form fields, useFormField
provides computed properties for validation state, error messages, and more. Can be very useful when you are using fields in child components of form.
To use the modes eager
, progressive
or blur
, you must use this useFormField
composable to add the necessary validation events.
Parameters
useFormField<T>
takes the following parameters:
name
:string
- The name of the field in the validation schema (must be a key from the schema).options
:FormFieldOptions<T>
(optional) - Field-specific options.defaultValue
:T
(optional) - The default value of the field.mode
:'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
(optional) - The validation mode for the field - see validation modesref
:Ref<HTMLElement | ComponentInstance>
(optional) - Vue ref to the component/element for automatic event binding - useuseTemplateRef()
for type safetyformIdentifier
:string | symbol
(optional) - Identifier for the form (must match the one used inuseFormValidator
)
Return
useFormField
returns an object containing:
errors
:ComputedRef<ValidationIssues>
- Validation errors for this field.errorMessage
:ComputedRef<string>
- The first validation error message.isValid
:ComputedRef<boolean>
- Indicates if the field is valid.isDirty
:ComputedRef<boolean>
- Indicates if the field has been modified.isBlurred
:ComputedRef<boolean>
- Indicates if the field has lost focus.hasError
:ComputedRef<boolean>
- Indicates if the field has errors.isValidated
:ComputedRef<boolean>
- Indicates if the field has been validated.isValidating
:ComputedRef<boolean>
- Indicates if the field is currently being validated.mode
:ComputedRef<StrictOptions['mode']>
- The validation mode for the field.value
:WritableComputedRef<T>
- The reactive value of the field with proper TypeScript typing.validationEvents
:ComputedRef<{ onBlur?: () => void; }>
- Validation events to bind to the field. They are used to trigger field validation, to be used like thisv-bind="validationEvents"
(components must emitblur
event to trigger field validation) - Not necessary forlazy
,aggressive
validation modes or if you use the component reference when initializing the composable.
Recent Improvements (v4.0.0)
🚀 Enhanced Type Safety
- Automatic schema inference: Use
typeof schema
for precise TypeScript types - Field-level type safety:
useFormField<T>
provides exact field types - Improved reactivity: Optimized watchers with better performance and memory management
🎯 Better Interactive Element Detection
The ref
option in useFormField
now automatically detects and binds events to:
- Standard form elements:
input
,select
,textarea
,button
- Focusable elements: links with
href
, elements withtabindex
- ARIA interactive elements:
role="button"
,role="textbox"
, etc. - Custom interactive elements:
data-interactive
,data-clickable
,.interactive
🔧 Improved Memory Management
- Automatic cleanup of event listeners to prevent memory leaks
- WeakMap-based tracking for better garbage collection
- Race condition protection in async validation
📝 Better Development Experience
- More informative warning messages
- Improved error handling and validation states
- Enhanced debugging capabilities
Performance & Best Practices
🚀 Performance Tips
- Use
throttledFields
ordebouncedFields
for expensive validations or network requests - Prefer
eager
orprogressive
modes for better UX instead ofaggressive
- Use
lazy
mode for simple forms with minimal validation - Leverage TypeScript: Always use
typeof schema
for automatic type inference
💡 Common Patterns
Multiple Forms on Same Page
const form1 = useFormValidator<typeof schema1>({
schema: schema1,
options: { identifier: 'form-1' }
})
const form2 = useFormValidator<typeof schema2>({
schema: schema2,
options: { identifier: 'form-2' }
})
// Use matching identifiers in useFormField
const { value } = useFormField<string>('name', {
formIdentifier: 'form-1'
})
Custom Interactive Elements
<template>
<!-- Add data-interactive for custom components -->
<div data-interactive class="custom-input" tabindex="0">
Custom Input
</div>
</template>
⚠️ Common Pitfalls
- Mismatched form identifiers: Ensure
useFormField
uses the sameformIdentifier
asuseFormValidator
- Missing refs for interactive modes:
eager
,blur
, andprogressive
modes require eitherref
orvalidationEvents
- Incorrect TypeScript generics: Always specify both schema and field name:
useFormField<T>
Troubleshooting
Type Errors
Problem: WritableComputedRef<string | number | boolean | undefined>
// ❌ Wrong - loses type precision
const { value } = useFormField('name')
// ✅ Correct - precise typing
const { value } = useFormField<string>('name')
Using useTemplateRef
with useFormField
cause TypeScript errors
Cause: useTemplateRef
can create TypeScript circular references when the destructured variable name resembles the template ref name.
If you encounter TypeScript errors when using useFormField
with useTemplateRef
, use classic ref()
instead:
// ❌ May cause TypeScript errors
const { value: email } = useFormField<string>('email', {
ref: useTemplateRef('emailRef'),
})
// ✅ Correct - precise typing
const { value: email } = useFormField<string>('email', {
ref: useTemplateRef<string>('emailRef'),
})
// ✅ Use classic `ref()` instead
const emailRef = ref<HTMLInputElement>()
const { value: email } = useFormField<string>('email', {
ref: emailRef,
})
Validation Not Triggering
Problem: Field validation doesn't work with eager
/blur
/progressive
modes
// ❌ Missing ref or validation events
const { value } = useFormField<string>('name')
// ✅ Use ref for automatic detection
const { value } = useFormField<string>('name', {
ref: useTemplateRef('inputRef')
})
// ✅ Or use validation events manually
const { value, validationEvents } = useFormField<string>('name')
// Then: v-bind="validationEvents" on your component
Element Not Found Warning
Problem: No element found for ref in field 'name'
Solutions:
- Ensure the ref is properly bound to an HTML element or Vue component
- Make sure the component has a
$el
property if it's a Vue component - Use
data-interactive
attribute for custom interactive elements
Types
FormValidatorOptions
interface FormValidatorOptions {
/**
* Validation mode
* - lazy: validate on input value change
* - aggressive: validate all fields immediately on form creation and on input value change
* - blur: validate on blur
* - eager: validate on blur at first (only if the field is not empty) and then on input value change
* - progressive: The field becomes valid after the first successful validation and then validated on input value change. If the field is invalid, the error message on the first blur event.
* @default 'lazy'
*/
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
/**
* Fields to validate with throttling
* Useful for fields that require a network request to avoid spamming the server
* @example { name: 1000 } or { name: true } for the default throttle time (1000ms)
*/
throttledFields?: Partial<Record<ModelKey, number | true>>
/**
* Fields to validate with debouncing
* Useful to wait for the user to finish typing before validating
* Useful for fields that require a network request to avoid spamming the server
* @example { name: 300 } or { name: true } for the default debounce time (300ms)
*/
debouncedFields?: Partial<Record<ModelKey, number | true>>
/**
* Scroll to the first error found
* @default '.has-field-error'
*/
scrollToError?: string | false
/**
* Identifier to use for the form
* Useful to have multiple forms on the same page
* @default `main-form-validator`
*/
identifier?: string | symbol
}
FormFieldOptions
interface FormFieldOptions<T> {
/**
* Default value of the field
* @default undefined
*/
defaultValue?: T
/**
* Validation mode
* To override the form validation mode
*/
mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
/**
* Vue ref to the component or HTML element for automatic event binding
* Use useTemplateRef() for type safety
* Automatically detects interactive elements (input, select, textarea, button, ARIA elements, etc.)
* Necessary for 'eager', 'progressive' and 'blur' validation modes
*/
ref?: Ref<HTMLElement | ComponentInstance>
/**
* Identifier for the form
* Useful when you have multiple forms on the same component
* Should be the same as the one used in `useFormValidator`
*/
formIdentifier?: string | symbol
}