Custom components
Any control can work with NotForm. You only need to answer two questions about it:
- How does it take and emit its value? Usually
v-model. - Which of its events mean "typed", "committed", and "left"? These map to
onInput,onChange, andonBlur.
Choose the binding
| The control... | Bind it like this |
|---|---|
Forwards native input, change, and blur events | v-model="field.model" v-bind="field.events" |
Emits only update:modelValue when a value is picked | v-model="field.model" @update:model-value="field.events.onChange" |
Emits update:modelValue on every keystroke | v-model="field.model" @update:model-value="field.events.onInput" |
Emits an event when focus leaves, such as blur, focusout, or close | @blur="field.events.onBlur" |
Use the event names from the component's own documentation.
Which handler means what
onInput: the value changed while the user is still editing, such as typing. By default it only validates if the field already shows an error.onChange: the user committed a value, such as selecting an option. It validates by default.onBlur: focus left the control. It marks the field touched and validates by default.
Pick the handler that matches the interaction. You can change what each one triggers with validateOn and validationMode.
v-model before the event binding. The value has to be written before onChange or onInput takes its snapshot for validation.Controls that emit a value
<template>
<Field
v-slot="{ field }"
:field="form.field('country')"
>
<CountryPicker
v-model="field.model"
:invalid="!field.isValid"
@update:model-value="field.events.onChange"
@focusout="field.events.onBlur"
/>
<FieldError :errors="field" />
</Field>
</template>
Controls that forward native events
<template>
<Field
v-slot="{ field }"
:field="form.field('displayName')"
>
<TextInput
v-model="field.model"
v-bind="field.events"
/>
<FieldError :errors="field" />
</Field>
</template>
field.events contains onBlur, onChange, and onInput. If the component does not forward those events, bind the callbacks explicitly, as above.
Controls that do not use v-model
If a control exposes a value prop and a callback instead, write through field.model yourself:
<template>
<RatingInput
:value="field.model"
@rate="(value) => {
field.model = value
field.events.onChange()
}"
/>
</template>
Assign first, then call the event.
A reusable field wrapper
Most apps wrap label, input, and error once. Accept a field instance as a prop.
<script setup lang="ts">
import { FieldError, type FieldInstance } from 'notform'
defineProps<{
field: FieldInstance<string>
label: string
type?: string
}>()
</script>
<template>
<div class="field">
<label :for="field.path">{{ label }}</label>
<input
:id="field.path"
v-model="field.model"
v-bind="field.events"
:type="type ?? 'text'"
:aria-invalid="!field.isValid"
>
<FieldError
:errors="field"
class="field-error"
role="alert"
/>
</div>
</template>
<template>
<AppTextField
:field="form.field('email')"
label="Email"
type="email"
/>
</template>
The parent keeps the form instance and passes only the field. Array items are fields too, so <AppTextField :field="item.field('email')" /> works inside ArrayField.
Checklist for a new control
- Does
field.modelreflect the control's value type? Make sure the schema accepts that type (for exampleboolean,string[], or a number). - Does the control have an element that can take focus, so
onBlurcan fire? - Does it have an accessible name? Use
field.pathfor ids. - Does it show
field.isValidand the error message?