useNotForm
useNotForm is the core composable for creating and managing a NotForm instance.
It connects a Standard Schema-compatible validator to a deeply reactive form state and provides everything needed to manage values, validation, field interaction, errors, submission, and reset behavior.
The returned form instance can be used directly or provided to <NotForm> for use by descendant field components.
Signature
function useNotForm<TSchema extends ObjectSchema>(
config: UseNotFormConfig<TSchema>,
): NotFormInstance<TSchema>
The generic type is inferred from the supplied schema.
The schema drives the types of:
form.valuesform.setValue()- field paths
- validation results
onSubmit()form.validateField()form.reset()
Configuration
useNotForm accepts a UseNotFormConfig object.
schema
schema: MaybeRefOrGetter<TSchema>
The validation schema used to parse and validate form data.
The schema can be provided directly or through a Vue ref or getter.
import { z } from 'zod'
const schema = z.object({
email: z.email(),
password: z.string().min(8),
})
const form = useNotForm({
schema,
onSubmit(values) {
console.log(values)
},
})
The schema also determines the output passed to onSubmit.
For example, a schema can transform its input:
const schema = z.object({
age: z.coerce.number(),
})
const form = useNotForm({
schema,
onSubmit(values) {
// `values.age` is the schema's output type
console.log(values.age)
},
})
initialValues
initialValues?: DeepPartial<StandardSchemaV1.InferInput<TSchema>>
The initial values for the form.
The supplied values become the form's initial baseline and are used for:
- populating
form.values - dirty-state comparisons
form.reset()
Because the type is deeply partial, nested values do not necessarily need to be supplied in full.
const form = useNotForm({
schema,
initialValues: {
email: 'user@example.com',
profile: {
name: 'Jane',
},
},
onSubmit(values) {
console.log(values)
},
})
When initialValues is omitted, the form starts from an empty object.
The stored baseline is exposed through form.initialValues.
initialErrors
initialErrors?: StandardSchemaV1.Issue[]
Initial validation issues for the form.
This is useful when a form needs to start with errors returned from another source, such as previously persisted validation state.
const form = useNotForm({
schema,
initialErrors: [
{
message: 'This email is already registered.',
path: ['email'],
},
],
onSubmit(values) {
console.log(values)
},
})
The errors become the form's initial error baseline and are exposed through form.initialErrors.
validateOn
validateOn?: Partial<Record<ValidationTrigger, boolean>>
Controls which interaction events trigger validation.
Available triggers are:
type ValidationTrigger
= | 'onBlur'
| 'onChange'
| 'onFocus'
| 'onInput'
| 'onMount'
The default configuration is:
{
onBlur: true,
onChange: true,
onFocus: false,
onInput: true,
onMount: false,
}
Only the values you provide are overridden.
For example:
const form = useNotForm({
schema,
validateOn: {
onBlur: true,
onInput: false,
},
onSubmit(values) {
console.log(values)
},
})
The resolved configuration is available through form.validateOn.
Individual field components can override supported triggers on a per-field basis.
validationMode
validationMode?: 'eager' | 'lazy'
Controls how validation is applied after field interaction.
The default is:
validationMode: 'eager'
eager
After a field has an error, subsequent changes can continue to validate it immediately.
lazy
Validation is deferred until blur or submission.
const form = useNotForm({
schema,
validationMode: 'lazy',
onSubmit(values) {
console.log(values)
},
})
The resolved mode is exposed through form.validationMode.
onSubmit
onSubmit?: (
values: StandardSchemaV1.InferOutput<TSchema>
) => Promise<void> | void
Called after the complete form has been validated successfully.
The callback receives the schema's output type, not necessarily its raw input type.
const form = useNotForm({
schema,
async onSubmit(values) {
await saveUser(values)
},
})
onSubmit is optional. A form can still be created and validated without providing a submit callback.
Returned Form Instance
useNotForm returns a NotFormInstance.
const form = useNotForm({
onSubmit,
schema,
})
The instance contains the form's baseline state, reactive values, validation configuration, field tracking, errors, validation methods, submission state, and reset API.
Baseline State
initialValues
readonly initialValues: DeepPartial<StandardSchemaV1.InferInput<TSchema>>
The values currently used as the form's baseline.
These are the values used when determining whether fields are dirty.
form.initialValues
Calling reset() without arguments restores these values.
When new values are provided to reset(), they replace this baseline.
initialErrors
readonly initialErrors: StandardSchemaV1.Issue[]
The errors currently used as the form's error baseline.
form.initialErrors
Calling reset() restores these errors.
Providing new errors to reset() replaces this baseline.
Validation Configuration
validateOn
readonly validateOn: {
onBlur: boolean
onChange: boolean
onFocus: boolean
onInput: boolean
onMount: boolean
}
The resolved validation triggers for the form.
Unlike the optional validateOn passed to useNotForm, this property always contains all trigger keys.
console.log(form.validateOn.onBlur)
console.log(form.validateOn.onInput)
validationMode
readonly validationMode: 'eager' | 'lazy'
The resolved validation mode for the form:
console.log(form.validationMode)
Values
values
values: StandardSchemaV1.InferInput<TSchema>
values is the deeply reactive object containing the current form values.
It can be accessed directly:
console.log(form.values.email)
Because it is reactive, it can also be used directly with v-model:
<input v-model="form.values.email" />
Nested values work the same way:
<input v-model="form.values.address.city" />
setValue(path, value)
setValue(
path: Paths<StandardSchemaV1.InferInput<TSchema>>,
value: Get<StandardSchemaV1.InferInput<TSchema>, path>,
): void
Sets a value using a dot-separated field path.
This is useful for custom inputs or deeply nested values where v-model is not appropriate.
form.setValue('email', 'user@example.com')
form.setValue('address.city', 'Lagos')
The field path and value are typed from the schema.
setValue() also updates dirty tracking by comparing the new value with the current initialValues.dirtyFields.setValue() does not itself trigger validation. Validation is driven by the configured field interaction handlers.Touched State
touchedFields
touchedFields: Set<Paths<StandardSchemaV1.InferInput<TSchema>>>
The set of field paths that have been marked as touched.
console.log(form.touchedFields)
A field is normally marked as touched by its NotField blur handler.
During submission, all existing field paths are marked as touched so validation feedback can surface across the form.
isTouched
isTouched: ComputedRef<boolean>
Indicates whether any field in the form has been touched.
<p v-if="form.isTouched">
The form has been touched.
</p>
In JavaScript or TypeScript, use .value:
if (form.isTouched.value) {
console.log('Form has been touched')
}
touchField(path)
touchField(
path: Paths<StandardSchemaV1.InferInput<TSchema>>,
): void
Marks a specific field as touched.
form.touchField('email')
This is normally handled automatically by field components but is useful for custom form controls.
Dirty State
dirtyFields
dirtyFields: Set<Paths<StandardSchemaV1.InferInput<TSchema>>>
The set of field paths whose values currently differ from initialValues.
console.log(form.dirtyFields)
isDirty
isDirty: ComputedRef<boolean>
Indicates whether any field is dirty.
<p v-if="form.isDirty">
You have unsaved changes.
</p>
In JavaScript:
if (form.isDirty.value) {
console.log('Form has unsaved changes')
}
dirtyField(path)
dirtyField(
path: Paths<StandardSchemaV1.InferInput<TSchema>>,
): void
Manually marks a field as dirty.
form.dirtyField('email')
Normal field interactions update dirty tracking automatically.
Errors
errors
errors: StandardSchemaV1.Issue[]
The raw validation issues produced by the most recent validation run.
console.log(form.errors)
Each issue contains the path and message supplied by the validation schema.
for (const error of form.errors) {
console.log(error.path)
console.log(error.message)
}
errorsMap
errorsMap: ComputedRef<
Partial<Record<Paths<StandardSchemaV1.InferInput<TSchema>>, string>>
>
A convenient flat map of field paths to their first active error message.
form.errorsMap.email
form.errorsMap['address.city']
For example:
<p v-if="form.errorsMap.email">
{{ form.errorsMap.email }}
</p>
errorsMap is useful when you only need the first message for a field.
Use errors or getFieldErrors() when you need the complete validation issues.
setError(error)
setError(error: StandardSchemaV1.Issue): void
Adds or replaces the error for a specific path.
If an issue already exists for the same normalized path, it is replaced.
form.setError({
message: 'This email is already registered.',
path: ['email'],
})
This is useful for server-side validation errors.
setErrors(errors)
setErrors(errors: StandardSchemaV1.Issue[]): void
Replaces all active errors.
form.setErrors([
{
message: 'Email is already registered.',
path: ['email'],
},
{
message: 'Username is already taken.',
path: ['username'],
},
])
clearErrors()
clearErrors(): void
Removes every active validation error.
form.clearErrors()
getFieldErrors(path)
getFieldErrors(
path: Paths<StandardSchemaV1.InferInput<TSchema>>,
): StandardSchemaV1.Issue[]
Returns all active validation issues associated with a particular field path.
const errors = form.getFieldErrors('email')
Unlike errorsMap, this does not discard additional issues for the same field.
for (const error of form.getFieldErrors('email')) {
console.log(error.message)
}
Validation
isValidating
isValidating: Ref<boolean>
Indicates whether one or more validation operations are currently running.
<p v-if="form.isValidating">
Validating...
</p>
The state remains active while concurrent validation operations are still pending.
validate()
validate(): Promise<
StandardSchemaV1.Result<
StandardSchemaV1.InferOutput<TSchema>
>
>
Validates the entire form against the current schema.
const result = await form.validate()
When validation succeeds:
const result = await form.validate()
if ('value' in result) {
console.log(result.value)
}
When validation fails, the returned result contains issues and those issues replace the form's current errors.
A successful validation clears the current errors.
validateField(path)
validateField(
path: Paths<StandardSchemaV1.InferInput<TSchema>>,
): Promise<StandardSchemaV1.Result<...>>
Validates a single field against the complete schema.
const result = await form.validateField('email')
validateField() only replaces errors associated with that field.Form Validity
isValid
isValid: ComputedRef<boolean>
Indicates whether the form currently has no active validation errors.
<button
type="submit"
:disabled="!form.isValid"
>
Submit
</button>
In JavaScript:
if (form.isValid.value) {
console.log('The form is valid')
}
isValid is based on the current errors collection. It does not independently execute validation.
Submission
isSubmitting
isSubmitting: Ref<boolean>
Indicates whether the form's onSubmit handler is currently running.
<button
type="submit"
:disabled="form.isSubmitting"
>
{{ form.isSubmitting ? 'Submitting...' : 'Submit' }}
</button>
submit(event)
submit(event: SubmitEvent): Promise<void>
Submits the form.
The method:
- Prevents the browser's native form submission.
- Marks all existing field paths as touched.
- Marks all existing field paths as dirty.
- Validates the complete form.
- Aborts when validation fails.
- Calls
onSubmitwith the schema's validated output when validation succeeds.
Bind it directly to a native <form>:
<form @submit="form.submit">
...
</form>
Example:
<form @submit="form.submit">
<input v-model="form.values.email" />
<button
type="submit"
:disabled="form.isSubmitting"
>
{{ form.isSubmitting ? 'Submitting...' : 'Submit' }}
</button>
</form>
When validation fails, onSubmit is not called.
When no onSubmit callback was provided, the form still performs validation but has no submission callback to execute.
Reset
reset()
reset(
values?: DeepPartial<StandardSchemaV1.InferInput<TSchema>>,
errors?: StandardSchemaV1.Issue[],
): void
Resets the form and clears all touched and dirty tracking.
Calling reset() without arguments restores the current initialValues and initialErrors:
form.reset()
Reset With New Values
Pass new values to replace the current baseline:
form.reset({
email: 'jane@example.com',
name: 'Jane',
})
Those values become the new initialValues.
A later call to:
form.reset()
will restore these values.
Reset With New Errors
You can provide both values and errors:
form.reset(
{
email: 'jane@example.com',
name: 'Jane',
},
[
{
message: 'Email requires verification.',
path: ['email'],
},
],
)
The supplied errors become the new initialErrors.
Calling reset() afterward restores both the new values and those errors.
Reset Only Values
Because the two parameters are positional, passing new values while keeping existing baseline errors is possible:
form.reset({
email: 'new@example.com',
})
The existing initialErrors remain unchanged.
Reset Only Errors
To replace only the baseline errors while leaving the current baseline values unchanged:
form.reset(undefined, [
{
message: 'Email requires verification.',
path: ['email'],
},
])