Displaying multiple error messages on incorrect form entries with Vee-validate | Vue
Timea Pentek
Posted on April 25, 2024
A typical interaction with websites involves filling in forms. Form validation and displaying errors on incorrect input entries will be a task to account for when building a webpage.
Form validation can be done both on the client side and the server side. Client-side validation is an initial check that happens in the browser before the data is sent to the server. It ensures that the requirements set are fulfilled by the data that is being entered, and it provides the user with an instant response in case an entry is incorrect.
According to MDN (https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation), client-side validation is not an exhaustive security measure as it is easy to by-pass, and apps should always perform security checks on data submitted on the server-side as well. Server-side validation is a check that occurs on the server after the data is submitted. Before the data is saved to the server it has to pass a server-side validation. If the data doesn’t pass the check, a response is sent back to the user with the corrections that should be made.
In this blog post I am exploring the client-side form validation with Vee-validate (https://vee-validate.logaretm.com/v4/) and vee-validate/rules (https://www.npmjs.com/package/@vee-validate/rules).
This blog post reviews the solution I implemented to destructuring the error message for a form in a Vue 3 application.
When an input field is being validated against multiple rules, what are the options for sending back an error response to the user, assuming the user added an incorrect value in the input field? Should we send back a single error message or do we destructure the error message and send back multiple descriptive error messages for each rule that failed?
It probably depends on the input field and what type of data is being asked, however generic messages such as ‘An error occurred’ lack context, and so we should always try to provide the user with enough details.
The password field is a common place where the user is required to meet multiple rules, and in my Vue application the input is validated against multiple criteria. Because of this, I decided to display multiple error messages for the password field, which is part of a registration form built with Vee-validate.
Why did I decide to display multiple error messages for the password field? The main reason behind my decision was user experience (UX). It can become annoying for the user to try and guess what went wrong and why the field is not accepting their input.
Let's get started!
To get started, I created a basic Vue 3 app. In the components folder, I generated a new file called AppForm.vue for the form template. I then added the AppForm component to the main App.js file.
Below is the form template that has three input fields and a submit button.
<form>
<!-- Email -->
<div class="mb-3">
<label class="inline-block mb-2 text-white">Email</label>
<input
name="email" type="email"
placeholder="Enter your email"
class="block w-full py-1.5 px-3 text-gray-800 border border-gray-300 transition duration-500 focus:outline-none focus:border-black rounded" />
</div>
<!-- Password -->
<div class="mb-3">
<label class="inline-block mb-2 text-white">Password</label>
<input
type="password"
placeholder="Create password"
class="block w-full py-1.5 px-3 text-gray-800 border border-gray-300 transition duration-500 focus:outline-none focus:border-black rounded" />
</div>
<!-- Confirm Password -->
<div class="mb-3">
<label class="inline-block mb-2 text-white">Confirm Password</label>
<input
name="confirm_password"
type="password" placeholder="Confirm password"
class="block w-full py-1.5 px-3 text-gray-800 border border-gray-300 transition duration-500 focus:outline-none focus:border-black rounded" />
</div>
<!-- Submit Btn -->
<button
type="submit"
class="block bg-[length:400px_200px] mt-6 w-52 bg-gradient- to-r from-[#1F1C2C] via-[#928DAB] to-[#1F1C2C] text-white py-1.5 px-3 rounded enabled:hover:bg-[right_center] enabled:transition-all enabled:duration-500">
Submit
</button>
</form>
To use the Vee-validate library, run in the terminal the following:
npm i vee-validate
In my project I also used TailwindCSS, just to be able to quickly style the form. I installed the necessary resources and made the configurations to set up Tailwind.
Next, I wanted to configure the Vee-validate library. To not clutter the App.js I decided to outsource this to a validation.js file that I created in a new folder called plugins, inside the src folder.
// path: src/plugins/validation.js
export default {
install(app) {}
}
In the configuration file, I am creating the Vee-Validate plugin. In Vue, plugins are objects with a method called install. When we register the plugin Vue will call the install method.
To register the plugin we have to add the following to our main.js file:
import VeeValidatePlugin from "./plugins/validation";
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.use(VeeValidatePlugin);
app.mount("#app");
Note: Your app may import other files such as CSS stylesheets.
The reason why I chose to set up VeeValidate this way is because the project I am working on has other forms throughout the app. This way VeeValidate will be available globally for me.
Next, I installed the vee-validate/rules package that includes the most common validators for input fields (https://www.npmjs.com/package/@vee-validate/rules).
npm install @vee-validate/rules
VeeValidate handles complex validations in an easy way. It uses the Vee-form, Vee-field, and ErrorMessage components to validate forms. In the configuration file, validation.js, I imported these components. After, I imported the validation rules and defined them using the defineRule function from vee-validate.
//import the components and defineRule function from vee-validate
import {
Form as VeeForm,
Field as VeeField,
ErrorMessage,
defineRule,
} from "vee-validate";
// import the validation rules from vee-validate/rules package
import {
required,
min,
max,
email,
confirmed,
not_one_of as excluded,
} from "@vee-validate/rules";
export default {
install(app) {
// Make components available globally in the Vue application using the app.component() method.
// The function takes the arguments: app.component("registered name", implementation)
app.component("VeeForm", VeeForm);
app.component("VeeField", VeeField);
app.component("ErrorMessage", ErrorMessage);
// Define the rules. The defineRule function accepts a rule name that acts as an identifier
// for that validation rule, the second argument is the validator function that will verify the field value.
defineRule("required", required);
defineRule("min", min);
defineRule("max", max);
defineRule("email", email);
defineRule("passwords_mismatch", confirmed);
defineRule("excluded", excluded);
},
};
The configuration for VeeValidate is complete for now. To implement it, I had to make a few changes to the form template in the AppForm.vue component. The first changes that I made were the following:
- Changing the form tag to vee-form
- Changing the input tags to vee-field. Note: Vee-field will by default render an input tag. In order to render a different field add the ‘as’ prop to tell the component which field it should render (eg. as=’select’)
- Adding an ErrorMessage component to each input. The name attribute on the ErrorMessage component has to match the name attribute on the vee-field. This is how to tell vee-validate for which input field should the ErrorMessage display the messages for
The vee-validate rules can be added in two different ways, either by adding the rule directly to the input field or by outsourcing the rules to an object. I opted for outsourcing the rules to a schema object to keep the form as clean as possible. To achieve this I used the validation-schema prop on the vee-form and passed in an object called schema. With all these changes my form looked something like this:
<div>
<vee-form :validation-schema="schema">
<!-- Email -->
<div class="mb-3">
<label class="inline-block mb-2 text-white">Email</label>
<vee-field
name="email"
type="email"
placeholder="Enter your email"
class="block w-full py-1.5 px-3 text-gray-800 border border-gray-300 transition duration-500 focus:outline-none focus:border-black rounded"
/>
<ErrorMessage class="text-[#CF6679]" name="email" />
</div>
<!-- Password -->
<div class="mb-3">
<label class="inline-block mb-2 text-white">Password</label>
<vee-field
name="password"
type="password"
placeholder="Create password"
class="block w-full py-1.5 px-3 text-gray-800 border border-gray-300 transition duration-500 focus:outline-none focus:border-black rounded"
/>
<ErrorMessage class="text-[#CF6679]" name="password" />
</div>
<!-- Confirm Password -->
<div class="mb-3">
<label class="inline-block mb-2 text-white">Confirm Password</label>
<vee-field
name="confirm_password"
type="password"
placeholder="Confirm password"
class="block w-full py-1.5 px-3 text-gray-800 border border-gray-300 transition duration-500 focus:outline-none focus:border-black rounded"
/>
<ErrorMessage class="text-[#CF6679]" name="confirm_password" />
</div>
<!-- Submit Btn -->
<button
type="submit"
class="block bg-[length:400px_200px] mt-6 w-52 bg-gradient-to-r from-[#1F1C2C] via-[#928DAB] to-[#1F1C2C] text-white py-1.5 px-3 rounded enabled:hover:bg-[right_center] enabled:transition-all enabled:duration-500"
>
Submit
</button>
</vee-form>
</div>
Next, I filled out the schema object inside the AppForm.vue component.
<script>
export default {
name: "AppForm",
data() {
return {
schema: {
email: "required|email|min:3|max:100",
password: "required|min:7|max:50|excluded:password,qwerty",
confirm_password: "passwords_mismatch:@password",
},
};
},
};
</script>
To add the rules to the schema object, you create key-value pairs, where the key is the name attribute on the input, and the value is the rule/s. The form has 3 input fields or vee-fields, so I have three key-value pairs in the schema object. Each rule has to be separated by a vertical line ‘|’.
Below are a few short descriptions of what each rule means:
- Required: field has to be filled out
- Email: input value has to be of type email
- Min: the minimum amount of characters a value has to have
- Max: the maximum amount of characters a value can have
- Excluded: (the validator function is called not_one_of, but I imported it as excluded for easier comprehension) the input can’t have the values specified. In the form I created the password field can’t have the values ‘password’ or ‘qwerty’
- passwords_mismatch:(the validator function is called confirmed) the input must be the same value as the confirmation field. The confirmation field is passed in after the ‘@’ sign.
At this point, on an incorrect entry, an error message would appear under the input field saying ‘field is not valid’. The user does not know the rules that an input field’s value is validating against, so it can become unclear as to why the value is wrong and why the system is rejecting it. This can quickly become annoying.
To avoid bad user experience I decided to display each error separately. The number of errors that would appear depends on how many validation rules the input breaks. This is not the default behaviour for vee-validate because vee-validate opts for a fast exit strategy for better performance. This means that when vee-validate comes across the first rule that is not met, it does not bother checking the rest of the rules, instead it exits the validation process.
For this registration form I wanted to display all the error messages for the password field, so I made the following changes to the password field:
<!-- Password -->
<div class="mb-3">
<label class="inline-block mb-2 text-white">Password</label>
<vee-field name="password" :bails="false" v-slot="{ field, errors }">
<input
type="password"
placeholder="Create password"
class="block w-full py-1.5 px-3 text-gray-800 border border-gray-300 transition duration-500 focus:outline-none focus:border-black rounded"
v-bind="field"
/>
<div class="text-[#CF6679]" v-for="error in errors" :key="error">
{{ error }}
</div>
</vee-field>
</div>
Note: Don't forget to remove the ErrorMessage component that was added to the password field previously.
The first thing I did was to set the bails attribute on the vee-field to false. This lets vee-validate know that it has to check all rules even if it encounters one that already broke a validation rule. Basically, it cancels the fast exit strategy.
To render multiple error messages I had to tweak the behaviour of the vee-field component. The vee-field component makes use of scoped slots (v-slot) feature which allows rendering a complex group of markup. I created an opening and closing tag for the vee-field because I wanted to add more markup inside it, such as the input tag and the error messages. I moved the type, class, and placeholder attributes to the input tag. The default slot gave me access to the ‘field’ object and an ‘errors’ array. According to the VeeValidate docs:
The most crucial part of rendering fields with v-slot is that you bind the field object to your input element/input, the field object contains all the common attributes and listeners required for the field to be validated.
After I made these changes, I added a div tag, looped through the errors array, and displayed each message. (Docs for further explanation: https://vee-validate.logaretm.com/v4/api/field/#rendering-complex-fields-with-scoped-slots)
At this point, the error messages showed up separately. The next problem I had to solve was to convert the messages into descriptive information because right now it even looks like the application is broken.
I fixed this by adding custom error messages to the validation rules. Back in the validation.js, I imported the configure function from vee-validate, and for each rule I added a custom message.
configure({
generateMessage: (ctx) => {
const messages = {
required: `The field ${ctx.field} is required.`,
min: `The field ${ctx.field} is too short.`,
max: `The field ${ctx.field} is too long.`,
email: `The field ${ctx.field} must be a valid email.`,
excluded: `You are not allowed to use this value for the field ${ctx.field}.`,
passwords_mismatch: `The passwords don't match.`,
};
const message = messages[ctx.rule.name]
? messages[ctx.rule.name]
: `The field ${ctx.field} is invalid.`;
return message;
},
});
The generateMessage function comes from vee-validate and it has to return a string. To register the generateMessage function I used the configure function from vee-validate. I accessed the FieldContext (above as ‘ctx’) interface to have the information on the field name, value, and rule name (Docs for further explanation: https://vee-validate.logaretm.com/v4/guide/i18n#global-message-generator ). I then created an object called messages with the rule names and the custom error messages I wanted. I defined the message variable’s value using a quick ternary operator, and after I returned the correct string.
At last, the password field displays all the broken rules with a descriptive message.
Posted on April 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.