Validation Modes
NotForm separates when validation runs from how validation behaves after a field has been validated.
This gives you two controls:
validateOncontrols which interactions trigger validation.validationModecontrols when an errored field is validated again after it changes.
This distinction is important because input, blur, and change are not validation modes in the current API. They are validation triggers.
The Two Validation Controls
A form can be configured with:
const form = useNotForm({
schema,
validateOn: {
onBlur: true,
onChange: true,
onInput: true,
},
validationMode: 'eager',
onSubmit(values) {
console.log(values)
},
})
validateOn
validateOn determines which interactions can start validation.
NotForm supports five triggers:
| Trigger | When it runs |
|---|---|
onBlur | When a field loses focus |
onChange | When a field's value is committed |
onInput | When a field's value changes while the user is typing |
onFocus | When a field receives focus |
onMount | When a field is mounted |
The default configuration is:
{
onBlur: true,
onChange: true,
onFocus: false,
onInput: true,
onMount: false,
}
These defaults are resolved onto form.validateOn.
console.log(form.validateOn.onBlur)
console.log(form.validateOn.onInput)
validationMode
validationMode currently has two values:
type ValidationMode = 'eager' | 'lazy'
The default is:
validationMode: 'eager'
The mode does not select an event such as blur or input. Instead, it determines how validation behaves around existing errors.
Eager Mode
With eager, a field can continue validating during subsequent changes once it is in an errored state.
This is useful when you want the error to disappear as soon as the user fixes the value.
const form = useNotForm({
schema,
validationMode: 'eager',
validateOn: {
onBlur: true,
onInput: true,
},
onSubmit(values) {
console.log(values)
},
})
For example, imagine a required email field:
User focuses the field
↓
User leaves it empty
↓
Blur validation
↓
"Email is required"
↓
User starts typing
↓
Input validation continues
↓
Error disappears once the value validates
Eager mode is generally the better choice for forms where validation feedback should become progressively more responsive after an error has been shown.
Lazy Mode
With lazy, validation is deferred until blur or submission.
const form = useNotForm({
schema,
validationMode: 'lazy',
onSubmit(values) {
console.log(values)
},
})
This is useful when you want a less intrusive validation experience.
A user can type through an invalid intermediate state without constantly re-running validation.
For example:
User starts typing
↓
Intermediate value may be invalid
↓
No repeated validation while editing
↓
User leaves the field
↓
Validation runs
Lazy mode is especially useful for fields where valid input requires several keystrokes before the value becomes meaningful.
Demo
Choosing Validation Triggers
The triggers and mode can be combined.
Blur Validation
Blur validation is a good default for most form fields:
const form = useNotForm({
schema,
validateOn: {
onBlur: true,
onChange: false,
onInput: false,
},
onSubmit(values) {
console.log(values)
},
})
This avoids validating every intermediate value while the user is typing.
Input Validation
Enable onInput when feedback should happen while the value is being entered:
const form = useNotForm({
schema,
validateOn: {
onInput: true,
},
onSubmit(values) {
console.log(values)
},
})
This is useful for fields such as:
- password requirements
- usernames
- live formatting constraints
- short fields where immediate feedback is valuable
It can be expensive for asynchronous validation because every input interaction can start a validation run.
Change Validation
onChange validates when the value is committed.
const form = useNotForm({
schema,
validateOn: {
onChange: true,
},
onSubmit(values) {
console.log(values)
},
})
This can work well with selects, checkboxes, toggles, and other controls where a committed value is more meaningful than every intermediate interaction.
Focus Validation
Focus validation is available when a field needs to validate as soon as it receives focus:
const form = useNotForm({
schema,
validateOn: {
onFocus: true,
},
onSubmit(values) {
console.log(values)
},
})
It is disabled by default.
Mount Validation
onMount validates when a field is mounted.
const form = useNotForm({
schema,
validateOn: {
onMount: true,
},
onSubmit(values) {
console.log(values)
},
})
This can be useful for forms whose initial state should immediately be evaluated.
It is disabled by default.
Combining Triggers
Triggers are independent.
You can enable several at once:
const form = useNotForm({
schema,
validateOn: {
onBlur: true,
onChange: true,
onInput: true,
},
onSubmit(values) {
console.log(values)
},
})
Or selectively disable one:
const form = useNotForm({
schema,
validateOn: {
onBlur: true,
onChange: true,
onInput: false,
},
onSubmit(values) {
console.log(values)
},
})
Because validateOn is partial, only the triggers you specify are overridden.
Per-Field Configuration
Individual fields can override the form-wide trigger configuration.
<NotField
path="username"
:validate-on="{ onInput: true }"
v-slot="{ events }"
>
<input
v-model="form.values.username"
v-bind="events"
/>
</NotField>
This lets the form keep one general strategy while giving specific fields different behavior.
For example:
Form
├── name → blur
├── email → blur
├── username → input
└── password → blur
See <NotField> for field-level configuration.
Validation on Submission
Submission always performs complete form validation.
<form @submit="form.submit">
...
</form>
form.submit():
- prevents the browser's native submission
- marks all existing fields as touched
- marks all existing fields as dirty
- validates the entire form
- aborts when validation fails
- calls
onSubmitonly when validation succeeds
This means the validation configuration cannot be used to bypass submit-time validation.
A field that has never been interacted with is still validated when the form is submitted.
Manual Validation
Validation does not have to come from field events.
Validate the Whole Form
const result = await form.validate()
A successful result contains value:
const result = await form.validate()
if ('value' in result) {
console.log(result.value)
}
A failed result contains issues:
const result = await form.validate()
if ('issues' in result) {
console.log(result.issues)
}
form.errors is updated to match the complete validation result.
Validate a Single Field
const result = await form.validateField('email')
validateField() runs the schema against the form values but only updates errors associated with the requested field.
Existing errors for other fields are preserved.
const result = await form.validateField('email')
if ('issues' in result) {
console.log(result.issues)
}
This makes it useful for custom inputs and programmatic field validation.
Async Validation
NotForm supports asynchronous Standard Schema validation.
For example:
const schema = z.object({
username: z.string().refine(
async (value) => {
const response = await fetch(`/api/check-username?username=${value}`,)
const data = await response.json()
return data.available
},
{
message: 'Username is already taken',
},
),
})
Because validation can be asynchronous, the form exposes:
form.isValidating
Use it to display loading state:
<p v-if="form.isValidating">
Checking username...
</p>
isValidating remains active while validation work is still running, including concurrent validation runs.
Validation State
Validation produces several pieces of state:
form.errors
form.errorsMap
form.isValid
form.isValidating
Use errors when you need the complete StandardSchemaV1.Issue[].
Use errorsMap when you need the first message for a field.
Use isValid when you only need to know whether the form currently has active errors.
Use isValidating when displaying asynchronous validation state.
Recommended Strategies
General Forms
A good general-purpose configuration is:
const form = useNotForm({
schema,
validateOn: {
onBlur: true,
onChange: true,
onInput: true,
},
validationMode: 'eager',
onSubmit(values) {
console.log(values)
},
})
Less Intrusive Forms
Use lazy validation when you want fewer validation runs while editing:
const form = useNotForm({
schema,
validationMode: 'lazy',
validateOn: {
onBlur: true,
},
onSubmit(values) {
console.log(values)
},
})
Async-Heavy Fields
For expensive asynchronous validators, avoid unnecessarily aggressive input validation:
const form = useNotForm({
schema,
validateOn: {
onBlur: true,
onInput: false,
},
validationMode: 'lazy',
onSubmit(values) {
console.log(values)
},
})