Appearance
Validation using VeeValidate + Zod (A)
This is one of the patters to validate forms using VeeValidate and Zod, there are other patters too, which will be covered in the future.
This also works for Ionic Vue 3
VeeValidate and Zod is already integrated on most projects but if you need to integrate it manually, you need to install the following packages:
bash
## With Yarn
yarn add vee-validate zod @vee-validate/zod
# or with NPM
npm install vee-validate zod @vee-validate/zod
# or with Pnpm
pnpm add vee-validate zod @vee-validate/zodOverview
We do validations using the Composition API instead of using the Components provided by VeeValidate. This is because we are using Component UI libraries like Ionic and PrimeVue and this also provides us more control over the validation process.
General Usage
Integrating with VeeValidate and Zod requires us to change the way we define v-model bindings so there will be some changes in the way we define our forms.
Step 1: Define Validation Schema using Zod
First we need to define the validation schema using Zod. This is a simple example of a validation schema for a basic form:
javascript
// File: validators/company.validator.ts
import { toTypedSchema } from '@vee-validate/zod';
import * as zod from 'zod';
const COMPANY_NAME_MAX_LENGTH = 20;
// In this example, we used the name `schema`. But in the real world,
// you need to use appropriate name for your schema for better code readability.
export const validationSchema = zod.object({
companyName: zod.string()
.min(3, {
message: 'Custom error message for min',
})
.refine(val => val.length <= COMPANY_NAME_MAX_LENGTH, (val) => {
return {
message: `Custom error message for max. Current length: ${val.length}/${COMPANY_NAME_MAX_LENGTH}`,
};
}),
date: zod.date(),
email: zod.string()
.min(1, 'required')
.email({ message: 'Invalid email' }),
quantity: zod.number()
.gt(0, { message: 'Quantity must be greater than 0' }),
})Let's break down the code above:
We define a validation schema using Zod. We use the
objectmethod to define the schema. Theobjectmethod accepts an object with the fields and their respective validation rules.We use the
stringmethod to define the validation rules for thecompanyNamefield. We use theminmethod to define the minimum length of the string and we also modified the error message for theminmethod.Next, is the
.refine()method which is a little advanced. This allows us to define a custom validation rule and also get the context/current value of the field. In this example, we used it to define a custom validation rule for the maximum length of thecompanyNamefield and also modified the error message.
js
companyName: zod.string()
.min(3, {
message: 'Custom error message for min',
})
.refine(val => val.length <= COMPANY_NAME_MAX_LENGTH, (val) => {
return {
message: `Custom error message for max. Current length: ${val.length}/${COMPANY_NAME_MAX_LENGTH}`,
};
}),- The rest of the fields are self-explanatory.
js
date: zod.date(),
email: zod.string()
.min(1, 'required')
.email({ message: 'Invalid email' }),
quantity: zod.number()
.gt(0, { message: 'Quantity must be greater than 0' }),- We used the
datemethod to define the validation rules for thedatefield, theemailmethod for theemailfield, and thenumbermethod for thequantityfield. We do modified the error message for majority of the fields in this example.
Step 2: Use the Validation Schema
In this part, we are going to use the VeeValidate Composition API to define the form and it's bindings.
We may no longer use ref to define local variables for the form fields. Instead we will use the API provided by VeeValidate to define the form and it's bindings. But these API still uses the ref under the hood.
js
const { handleSubmit, errors, values: currentFormValues } = useForm({
validationSchema: validationSchema,
// Example if we want to set initial values.
initialValues: {
companyName: 'Initial Values are optional',
}
});
const { value: companyName } = useField<string>('companyName');
const { value: email } = useField<string>('email');
const { value: date } = useField<Date>('date');
const { value: qty } = useField<number>('quantity'); // Map to `quantity` field
const { value: price } = useField<number>('price');
function onSubmitButtonClicked() {
console.log('Form submitted', currentFormValues);
handleSubmit((payload) => {
const requestBody = {
payload,
date: formatDate(payload.date, 'YYYY-MM-DD'),
// map to `quantity` field
qty: payload.quantity,
};
// eslint-disable-next-line no-alert
alert(`Submit: ${JSON.stringify(requestBody, null, 2)}`);
})();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Let's break down the code above:
We use the
useFormmethod to define the form and it's bindings. We pass thevalidationSchemaas the first argument and theinitialValuesas the second argument. TheinitialValuesis optional.We use the
useFieldmethod to define the bindings for the form fields. We pass the field name as the argument. We also define the name of the variable that will hold the value of the field. This will be used in thev-modelbinding:
js
const {
value: companyName
} = useField<string>('companyName');
const {
value: email
} = useField<string>('email');
const {
value: date
} = useField<Date>('date');1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- We use the
handleSubmitmethod to define the submit handler. We pass a callback function as the argument. This callback function will be called when the form is submitted. We also use theerrorsvariable to display the error messages for the form fields.
When using the handleSubmit, we can use the constant declaration for more succinct code:
js
const onSubmitButtonClicked = handleSubmit((payload) => {
console.log('Form submit?', payload);
const requestBody = {
payload,
date: formatDate(payload.date, 'YYYY-MM-DD'),
qty: payload.quantity,
};
// eslint-disable-next-line no-alert
alert(`Submit: ${JSON.stringify(requestBody, null, 2)}`);
});1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Alternatively, you can use a function declaration, just like in the initial example. But you need to call the function to execute the submit handler:
js
function onSubmitButtonClicked() {
handleSubmit((payload) => {
const requestBody = {
payload,
date: formatDate(payload.date, 'YYYY-MM-DD'),
qty: payload.quantity,
};
// eslint-disable-next-line no-alert
alert(`Submit: ${JSON.stringify(requestBody, null, 2)}`);
})();
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Step 3: Bind the Form Fields and use the Validation Errors
vue
<script setup lang="ts">
// ....
const { handleSubmit, errors } = useForm({ /* ... */ });
const { value: companyName } = useField<string>('companyName');
const { value: email } = useField<string>('email');
const onSubmitButtonClicked = handleSubmit((payload) => {
// ....
});
// ....
</script>
<template>
<div>
<InputText id="company_name" v-model="companyName" type="text"
:class="{ 'p-invalid': errors.companyName }"
/>
<small class="text-red-500">{{ errors.companyName }}</small>
<InputText id="email" v-model="email" type="text"
:class="{ 'p-invalid': errors.email }" />
<small class="text-red-500">{{ errors.email }}</small>
<!-- .... -->
<Button label="Submit Button" @click="onSubmitButtonClicked()" />
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Notice on the code above, we use the v-model binding to bind the form fields to the useField method without using ref() to define the local variables. We also use the errors variable to display the error messages for the form fields.
Examples
TODO: Add Stackblitz Example
1. Create prime-vue nuxt template on stackblitz
2. Install vee-validate, zod, @vee-validate/zod
3. Create the stackblitz example for each of the examples below.
4. Embed the stackblitz example on each of the examples.
Basic Validation
TODO: Stackblitz Example
Object Result
TODO: Stackblitz Example
Async Validation
TODO: Stackblitz Example
Resources
Veevalidate Docs:
Veevalidate Composition API:
Zod Itegration:
Zod Docs: