21

NotField

Renderless component for individual form fields with validation state and event handlers.

<NotField> is a renderless component that connects an individual field to NotForm's validation and state management.

It provides the field's current state, validation errors, a validation method, and event handlers that can be bound to native inputs or custom form components.

Props

PropTypeRequiredDescription
pathstringYesDot-separated path to the field within the form values.
formNotFormInstance<any>NoExplicit form instance to use. Takes priority over a NotForm ancestor and is required when using NotField outside a NotForm.
validateOnPartial<Record<ValidationTrigger, boolean>>NoOverrides individual form-wide validation triggers for this field.
debouncenumberNoDelays input- and change-triggered validation by the specified number of milliseconds.

path

The field path identifies the value managed by the field.

Simple fields use a single property name:

<NotField path="email">
  ...
</NotField>

Nested fields use dot notation:

<NotField path="user.email">
  ...
</NotField>

The same path is exposed through the default slot and can be used for labels, IDs, messages, and other field-specific elements.

form

By default, NotField uses the form instance provided by a surrounding NotForm.

You can explicitly provide a form instance when needed:

<NotField :form="form" path="email" v-slot="{ events }">
  <input v-bind="events" />
</NotField>

An explicit form takes priority over the form provided by a NotForm ancestor.

This is also required when using NotField as a standalone field outside of a NotForm.

validateOn

validateOn allows individual validation triggers to be overridden for a field.

Only the triggers you specify are overridden; all other form-wide settings remain unchanged.

<NotField
  path="username"
  :validate-on="{ onInput: true }"
/>

For example, if the form validates on blur, the field above will additionally validate on input.

debounce

debounce delays input- and change-triggered validation until the user stops interacting with the field for the specified number of milliseconds.

This is particularly useful for asynchronous validation where validating on every keystroke could result in excessive requests.

<NotField
  path="username"
  :debounce="400"
  v-slot="{ events }"
>
  <input
    v-model="form.values.username"
    v-bind="events"
  />
</NotField>
Blur and submit-triggered validation is not delayed by debounce.
Omit the prop or set it to 0 to disable debouncing.

Slot Props

The default slot receives the complete field state and event handlers.

<NotField v-slot="field" path="email">
  ...
</NotField>
PropTypeDescription
pathstringThe dot-separated path of the field.
valueanyThe current field value as a read-only snapshot.
errorsStandardSchemaV1.Issue[]Validation issues from the last validation run.
isValidbooleanWhether the field currently has no validation errors.
isTouchedbooleanWhether the field has been interacted with or the form has been submitted.
isDirtybooleanWhether the current value differs from the initial value.
isValidatingbooleanWhether asynchronous validation is currently running for the field.
validate() => ReturnType<NotFormInstance<TSchema>['validateField']>Manually triggers validation for the field.
eventsonBlur: () => void

onInput: () => void

onChange: () => void

onFocus: () => void
Event handlers used to connect the field to NotForm.

path

The field's path is available directly from the slot:

<NotField v-slot="{ path }" path="email">
  <label :for="path">Email</label>
</NotField>

value

value contains the current value of the field.

It is intended as a read-only snapshot for displaying or inspecting the current value.

<NotField v-slot="{ value }" path="email">
  <p>Current value: {{ value }}</p>
</NotField>
Do not mutate value directly or use it as the source for v-model.

For two-way binding, bind your input to the form's values instead:

<input v-model="form.values.email" />

errors

errors contains all validation issues reported for the field during the most recent validation run.

<NotField v-slot="{ errors }" path="email">
  <input v-model="form.values.email" />

  <ul v-if="errors.length">
    <li v-for="error in errors" :key="error.message">
      {{ error.message }}
    </li>
  </ul>
</NotField>
Each error is a StandardSchemaV1.Issue.

isValid

isValid indicates whether the field currently has no validation errors.

<NotField v-slot="{ isValid }" path="email">
  <span v-if="isValid">Valid</span>
</NotField>

isTouched

isTouched indicates whether the field has been interacted with or the form has been submitted.

This can be useful for displaying validation feedback only after the user has interacted with the field:

<NotField
  v-slot="{ isTouched, isValid, events }"
  path="email"
>
  <input
    v-model="form.values.email"
    v-bind="events"
  />

  <p v-if="isTouched && !isValid">
    Please enter a valid email address.
  </p>
</NotField>

isDirty

isDirty indicates whether the field's current value differs from its initial value.

<NotField v-slot="{ isDirty }" path="email">
  <span v-if="isDirty">Unsaved changes</span>
</NotField>

isValidating

isValidating is true while asynchronous validation is running for the field.

<NotField v-slot="{ isValidating }" path="username">
  <span v-if="isValidating">
    Checking username...
  </span>
</NotField>

This is particularly useful for fields that use asynchronous validators.

validate

validate manually triggers validation for the field.

This is useful for custom inputs that manage their own interaction events or when validation needs to be triggered programmatically from the field's UI.

<NotField v-slot="{ validate }" path="email">
  <button type="button" @click="validate">
    Validate email
  </button>
</NotField>

Events

The events slot prop contains the event handlers NotForm uses to track field interaction and trigger validation.

<NotField v-slot="{ events }" path="email">
  <input v-bind="events" />
</NotField>

The event object contains:

{
  onBlur: () => void,
  onInput: () => void
  onChange: () => void
  onFocus: () => void
}

Binding all events

For native inputs, the simplest approach is to spread the handlers:

<NotField v-slot="{ events }" path="email">
  <input
    v-model="form.values.email"
    v-bind="events"
    type="email"
  />
</NotField>

NotForm can then use the configured validation triggers and track the field's interaction state.

Binding individual events

For custom components, bind the relevant handlers individually:

<NotField v-slot="{ events }" path="country">
  <CustomCombobox
    v-on:focusout="events.onBlur"
    v-on:pick="events.onChange"
  />
</NotField>
The handlers don't require the original DOM event as an argument. They simply notify NotField that the corresponding interaction occurred.

Basic Usage

A typical field combines the slot props with a form input and NotMessage:

<NotField v-slot="{ events, path }" path="email">
  <label :for="path">Email</label>

  <input
    :id="path"
    v-model="form.values.email"
    v-bind="events"
    type="email"
  />

  <NotMessage :path="path" />
</NotField>

Nested Fields

NotField supports nested object paths using dot notation.

Given a schema such as:

const schema = z.object({
  user: z.object({
    email: z.email(),
    name: z.string(),
  }),
})

You can address nested fields directly:

<NotField v-slot="{ events, path }" path="user.name">
  <label :for="path">Name</label>

  <input
    :id="path"
    v-model="form.values.user.name"
    v-bind="events"
  />

  <NotMessage :path="path" />
</NotField>

<NotField v-slot="{ events, path }" path="user.email">
  <label :for="path">Email</label>

  <input
    :id="path"
    v-model="form.values.user.email"
    v-bind="events"
  />

  <NotMessage :path="path" />
</NotField>

Per-Field Validation

Use validateOn when a field needs different validation behavior from the rest of the form.

For example, a form might normally validate on blur while a username field also validates while the user types:

<NotField
  path="username"
  :validate-on="{ onInput: true }"
  v-slot="{ events }"
>
  <input
    v-model="form.values.username"
    v-bind="events"
  />
</NotField>

Because validateOn is partial, unspecified triggers continue to use the form-wide configuration.

Debounced Validation

For fields with expensive or asynchronous validation, combine validateOn with debounce:

<NotField
  path="username"
  :validate-on="{ onInput: true }"
  :debounce="400"
  v-slot="{ events, isValidating, errors }"
>
  <input
    v-model="form.values.username"
    v-bind="events"
  />

  <span v-if="isValidating">
    Checking...
  </span>

  <p v-for="error in errors" :key="error.message">
    {{ error.message }}
  </p>
</NotField>

Validation waits until the user stops typing for 400ms before running.

This helps prevent unnecessary validation calls while the user is entering a value.

Standalone Fields

NotField can be used outside a NotForm by providing a form instance explicitly:

<NotField
  :form="form"
  path="email"
  v-slot="{ events }"
>
  <input
    v-model="form.values.email"
    v-bind="events"
    type="email"
  />
</NotField>
The explicit form instance takes priority over any form provided through a NotForm ancestor.