Building Custom inputs
Imperative vs Declarative
So far we’ve been using useForm
to create forms and use defineField
to create field-binding objects to integrate them with our fields. However, that usually requires a lot of boilerplate code to create the binding object and the field component.
For example, this is how a 5-field form looks like with useForm
and defineField
:
jsconst { defineField } = useForm();
const email = defineField('email');
const firstName = defineField('firstName');
const lastName = defineField('lastName');
const password = defineField('password');
const passwordConfirm = defineField('passwordConfirm');
This can get ugly very quickly especially if you have a lot of field arrays or nested fields involved. This is one of the downsides of using an imperative API, but with useField
you can switch to declarative API and get rid of all that boilerplate code.
useField
is a composition function that is similar to useForm
. it makes it easier to create and manage input components. You should use it when creating custom input components and that means you’ve made the choice that vee-validate will be an integral part of your input component system.
Creating a custom input component
Let’s start with a simple example, we will create a InputText
component that represents a text field. It can be as simple as this:
vue<template>
<input v-model="value" />
{{ errorMessage }}
</template>
<script setup>
import { useField } from 'vee-validate';
const props = defineProps({
name: String,
});
// The `name` is returned in a function because we want to make sure it stays reactive
// If the name changes you want `useField` to be able to pick it up
const { value, errorMessage } = useField(() => props.name);
</script>
This works exactly the same way as with defineField
, but now since you have a vee-validate field component, you can use it directly in any component with a form context and it will just work:
Here is a live example:
Notice how much of the burden of defining fields went away as soon as switched to the declarative approach. This is where useField
really shines, but that’s just us getting started. Follow along in this guide to make the most out of useField
.
Validation
All previous examples have used the form’s validation schema to validate the individual fields, however, you can also define a validation schema for each field individually. The same types of validation libraries are supported.
Yup Typed Schema
You can only validate using field-level validation or form-level validation, you cannot mix the two approaches here.
Validating with yup
You can use Yup schemas to validate fields individually by passing the schema as the second argument to useField
.
Validating with Zod
You can use Zod, however, you will need to add @vee-validate/zod
to your dependencies.
sh# with npm
npm i @vee-validate/zod
# with pnpm
pnpm add @vee-validate/zod
# with yarn
yarn add @vee-validate/zod
Then you can wrap your zod field schemas with toTypedSchema
function. More on that here.
Validating with Valibot
You can use Valibot to validate your fields, however, you will need to add @vee-validate/valibot
to your dependencies.
sh# with npm
npm i @vee-validate/valibot
# with pnpm
pnpm add @vee-validate/valibot
# with yarn
yarn add @vee-validate/valibot
Then you can wrap your valibot field schemas with toTypedSchema
function. More on that here.
Other validation providers
Validating with global validators
Another option is using @vee-validate/rules
which have been historically bundled with past versions of vee-validate. It includes rules that can be defined globally and then used anywhere using Laravel-like string expressions.
You can refer to the full guide on global rules here.
Validating with functions
Another option is to just use any 3rd party validation tool you prefer.
Triggers
Default Behavior
By default, vee-validate runs validation whenever the value
ref changes whether it was bound by a v-model
or changed in your code:
jsconst { value } = useField('fieldName', yup.string().required());
// validation WILL be triggered
value.value = 'something';
You can disable that behavior by passing a validateOnValueUpdate
option set to false
:
jsconst { value } = useField('fieldName', yup.string().required(), {
validateOnValueUpdate: false,
});
// validation WILL NOT trigger
value.value = 'something';
Handling Events
useField()
composition function is not concerned with any events, it only validates whenever the value
ref changes. However, it gives you everything you need to set up your own validation experience.
The useField
function exposes some handler functions, each handling a specific aspect of the validation experience:
handleChange
: Updates the field value, can be configured to trigger validation or silently update the valuehandleBlur
: Updates themeta.touched
flag, doesn’t trigger validation.
jsconst { handleChange, handleBlur } = useField('someField');
In this example, we are validating on the input
event (when the user types), which would make the validation aggressive:
With a slight adjustment we can make our validation lazy by changing the listener to @change
(validates when the user leaves the control):
vue<div>
<input @change="handleChange" :value="value" type="text" />
<span>{{ errorMessage }}</span>
</div>
Note that handleChange
can be called anywhere, not just in the template, and not as just an event handler. You can use it to mutate the field value whenever you want, as an added bonus you can choose if handleChange
should trigger a validation or not.
jsconst { handleChange } = useField('someField');
// validates by default
handleChange('new value');
// validates
handleChange('new value', true);
// Doesn't validate
handleChange('new value', false);
Let’s say you want to validate on blur
instead. You can use the handleBlur
in a similar way. The main differences are:
handleBlur
doesn’t mutate thevalue
of the field. It only sets themeta.touched
totrue
.handleBlur
does not validate the current value by default, you have to passtrue
as a second argument to trigger validation.
With that info in mind, you can validate on blur
like so:
vue<div>
<input
:value="value"
type="text"
@change="handleChange"
@blur="handleBlur($event, true)"
/>
<span>{{ errorMessage }}</span>
</div>
As you can see, the useField
doesn’t care which events you use handleChange
for. This allows for greater flexibility that’s not possible with the <Field>
component, not as straightforward at least.
Consider this validation experience:
- Validate on Change/Blur initially (when the user leaves the control), let’s call this lazy mode.
- If the field is invalid, switch the validation to validate on input (when the user types), let’s call this aggressive mode.
- If the field is valid, go back to “lazy” mode, otherwise, be “aggressive”.
Implementing this requires some knowledge about how the v-on
(we can bind objects on it) handler works.
jsconst { errorMessage, value, handleChange } = useField(() => props.name, undefined, {
validateOnValueUpdate: false,
});
const validationListeners = {
blur: evt => handleBlur(evt, true),
change: handleChange,
input: evt => handleChange(evt, !!errorMessage.value),
};
Then in your template, you can use v-on
to add your listener object:
vue<input :value="value" v-on="validationListeners" type="text" />
Here is a full example:
v-model Support
The useField
function can automatically manage v-model
integration for you. Usually, you will need to do this in every component you create:
jsconst props = defineProps({
modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
Instead, you can let useField
do that for you by telling it to enable v-model
syncing:
jsconst props = defineProps({
modelValue: String,
});
const { value, errorMessage } = useField('fieldName', undefined, {
syncVModel: true,
});
Now whenever value
changes, you will emit an update:modelValue
event with the new value. This is useful when you want to use v-model
with your custom input component:
You can also use different prop names for the modelValue
, for example, v-model:text
can be implemented by passing the model name directly to syncVModel
.
jsconst props = defineProps({
text: String,
});
const { value, errorMessage } = useField('fieldName', undefined, {
syncVModel: 'text',
});
This will emit onUpdate:text
instead of onUpdate:modelValue
whenever the value
changes.
Displaying Error Messages
You’ve already seen how to display errors with useForm
. With useField
you can use errorMessage
ref:
jsconst { errorMessage, value } = useField('fieldName', yup.string().required());
// contains the error message if available
errorMessage.value;
In addition to this, you can get all errors for the field using the errors
ref which contains multiple error messages if applicable:
jsconst { errors, value } = useField('fieldName', yup.string().required());
// contains an array of error messages, otherwise empty array
errors.value;
Here is an example where each field displays its entire range of error messages:
Custom Field Labels
More often than not, your fields will have names with underscores or shorthands which isn’t very nice when showing in error messages, for example, you might have specific encoding to your field names because they might be generated by the backend. Ideally, you want to avoid having messages like:
txtThe down_p is required
And instead show something more meaningful to the user
txtThe down payment is required
You can do this in two ways depending on which validators you are using (yup or global validators).
Custom Labels with Yup
With yup it is very straightforward, you just need to call label()
after defining your field’s validations either at the field level or form level:
jsconst schema = Yup.object({
email_addr: Yup.string().email().required().label('Email Address'),
acc_password: Yup.string().min(5).required().label('Your Password'),
});
Custom Labels with Zod
Zod does not have a built-in label
method, but you can override the default error messages by passing a custom message to the chained validator.
jsconst schema = z.object({
email_addr: z.string().email({ message: 'Email Address be a valid email address' });
acc_password: z.string().min(5, { message: 'Password be at least 5 characters long' });
});
If you are interested in how to do the same for global validators check the i18n guide
Field-level Meta
Each field has metadata associated with it, the meta
property returned from useField
contains information about the field:
valid
: The current field validity, is automatically updated for you.touched
: If the field was touched, can be updated withsetTouched
onuseField
’s return value.dirty
: If the field value was updated, you cannot change its value.pending
: If the field’s validations are still running, useful for long-running async validation.initialValue
: The field’s initial value, isundefined
if you didn’t specify any.
jsconst { meta } = useField('fieldName');
meta.dirty;
meta.pending;
meta.touched;
meta.valid;
meta.initialValue;
This is the typescript interface for a field’s meta value
tsinterface FieldMeta {
dirty: boolean;
pending: boolean;
touched: boolean;
valid: boolean;
initialValue: any;
}
Just like how the form’s meta
is read-only, this is also read-only and you cannot change it directly. Actually, only the touched
meta value can be mutated using handleBlur
, all other meta values are automatically updated for you as the field validates or when it changes its value.
jsconst { meta, handleBlur } = useField('fieldName');
// updates meta.touched = true
handleBlur();
Valid Flag Combinations
Since the meta.valid
flag is initially true
(because it just means there are no errors yet), it would cause problems if you have a “success” UI state as an indicator.
To avoid this case you should combine the valid
flag with either meta.dirty
or meta.touched
to get an accurate representation. You will see that in action in the next example.
In the following example, we the various meta information flags to style the input with some styling.
Field Dirty Flag and Initial Values
Notice in the previous example, we passed an initialValue
, this is because the default field value is undefined
which may cause unexpected meta.dirty
results.
To get accurate results for the meta.dirty
flag, you must provide an initial value to your field even if the values are empty.
To reduce the verbosity of adding an initialValue
prop to each field, you could provide the initialValues
prop to your useForm
call instead.
Building checkboxes
Checkboxes are a hard type of input to implement, mainly because of the expectations about how it should mutate the form’s value. For example, a checkbox can be used to toggle between true
or false
values which is common with single checkboxes. But it can also be used in a group to act as a multi-select between multiple options. In that case, it adds or removes its own “checked” value to the group value.
This means checkboxes have three states to maintain:
- It’s own checked value. In HTML this is done with the
value
attribute for nativeinput[type="checkbox"]
elements. - The current form value and if it is a checkbox group or a single checkbox. This is usually the
modelValue
prop for components. - Whether it’s checked or not, if the checked value equals or is in the form value then it is checked. This should be computed based on the previous fact.
All of this can be hard to wrap your head around. However, useField
makes this easy as it already handles the nuances of checkboxes.
The useField
function accepts a type
option that you can use to tell vee-validate that the input type is a checkbox and also accepts a checkedValue
option.
tsconst { handleChange, checked } = useField('myCheckbox', undefined, {
type: 'checkbox',
checkedValue: 'opt1',
});
A simple integration with an input element would look like this:
Note that we are not using v-model
with the value
returned from useField
here. This is because handleChange
is better suited for checkboxes as it handles toggling the value on or off and is also aware of the form has other checkboxes so it also handles whether the value should be an array or a single value.
You can find a more advanced example of checkboxes on this page.
Next Step
You've learned to build your form, your custom inputs. Now it is a quick one to figure out how to build special components like submission buttons, reset buttons, error displays and more with these composable helpers.