Validating the Contact Form with Vuelidate | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)
In this lesson, we will learn how to carry out proper and performant form validations for the contact-creation form of our Google Contacts clone app. The form validations will be done with the help of the beautiful and eloquent Vuelidate
validation library for Vue.js applications.
At the end of this lesson, your form validation should work like the video below and your should understand all the concepts by reading the entire lesson.
Start by creating a new branch of your project:
# Make sure you are within your project
git checkout -b 04-validate-new-contact-creation-form
Setup
Let's install the Vuelidate library:
# Make sure you are in the root directory of your project, then: cd ui # change into the `ui` directory yarn add @vuelidate/core @vuelidate/validators # install the packages for Vuelidate
If you do not have the Vue.js devtools extension for your browser, please install one. Chrome and Microsoft Edge can use the same extension. Visit chrome.google.com/webstore/detail/vuejs-dev.. to download and install. We will be using the Vue.js devtools to learn how to analyse the validation errors in the Vue.js application.
- Open
ui/.eslintrc.js
. Add the following after Line 85:
"prefer-promise-reject-errors": "off",
+ "func-names": "off",
+ "no-console": "off",
+ "object-curly-newline": "off",
+ "comma-dangle": "off",
+ "no-useless-escape": "off",
// TypeScript
quotes: ["warn", "double", { avoidEscape: true }],
This will remove some of the unnecessary linting errors. Please refer to this snapshot as reference.
- Open
ui/quasar.conf.js
. We will add theNotify
plugin for in-app notifications. Update theframework
>config
property by addingnotify: { position: "top" }
. And updateframework
>plugins
toplugins: ["Notify"]
. Please refer to this snapshot of the file .
Opening the Vue.js Devtools
The Vue.js devtools is extension accessed within the Chrome devtools. Within your Google Contacts clone app, click the Menu button (top-right "|" icon) > More tools > Developer tool. Or press CTRL+Shift+I
. You will see Vue
as a tab on the top of the Chrome DevTools. Switch to the tab when you want to inspect your Vue.js components or events or Vuex mutation/state/getters.
Recommended reads
It is highly recommended that you study the Vuelidate
docs at vuelidate-next.netlify.app. The docs is short so you should be done within one hour. You could choose to study after completing this lesson so that you can relate the concepts in the docs with what you have learnt here.
Validating the New Contact
Form
Open ui/src/pages/contacts/CreateContact.vue
. Open the snapshot of the file. Copy the entire content of the snapshot into CreateContact.vue
. I will explain all the changes.
Beginning from the script
section. The validation starts by importing the validation packages from Vuelidate
:
import useVuelidate from "@vuelidate/core";
import { required, email, url, helpers, integer } from "@vuelidate/validators";
The first import statement brings in the useVuelidate
composable function since we will be using the validation library within the setup
function (i.e. composition API). The second import statement imports the built-in validators which will use to validate our fields.
In Line 69, a custom phoneNumberValidator
validator is defined via the helpers.regex()
function. A regular expression is provided as the only paramater to helpers.regex()
. Read more about regex-based validators.
const phoneNumberValidator = helpers.regex(
/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/
);
To validate your form with Vuelidate
and the composition API, you must define the validation rules
. From Line 194, a computed property is used to return an object containing definitions matching the nesting of our field definitions from Line 79. For example, the validation rule for the firstName
field is:
firstName: {
value: {
required: helpers.withMessage("First Name is required.", required),
},
},
The above matches the nesting of the field definition for firstName
:
firstName: {
label: "First Name",
required: true,
value: "",
icon: "person",
autocomplete: "given-name",
},
We aren't validating the firstName
property, rather, we are validating the value
property inside firstName
because the value
property stores the value of the firstName
field.
If we add a simpler field definition such as:
const form = reactive({
firstName: '',
surname: '',
...
})
Then, the validation rules could look like this:
const rules = computed(() => ({
firstName: {
required: helpers.withMessage("First Name is required.", required),
},
})
)
As you have noticed, to validate a field, you need to assign each validator as properties of the validation object assigned to the field. The above could be simplified as:
const rules = computed(() => ({
firstName: {
required,
},
})
)
In this case, we aren't defining any custom error message for the required
validator. Each built-in validator imported from the "@vuelidate/validators"
package has default messages. For the required
validator it will be: "Value is required"
. It is too generic for must use cases and, as a tutorial, I wanted to demonstrate how to assign custom error messages to built-in validators. helpers.withMessage()
function call is used to define the custom message. It takes the error message as the first value and the built-in validator as the second value. There is another overload for the function. Read more here.
On Line 254, the useVuelidate
composable is used to return the validation object (a Vuelidate instance) which will be returned from the setup
function and made available in our template
section for error display. The useVuelidate
function takes, at least, two parameters:
- The
rules
, - The
form
schema, - A
validationConfig
object. Details about thevalidationConfig
object here. More about providing global config to your Vuelidate instance.
More on the $autoDirty
property of validationConfig
The $autoDirty
property is used to automatically track changes to the v-model of each validated field in your form. By default, Vuelidate
doesn't track the changes and you are required to call the $touch()
function for each field after you edit the field. Calling $touch()
tells Vuelidate that that field has changed and that it should set the $dirty
flag for the field to true
. Without the $dirty
flag being true
, the field won't be marked as having an error, i.e., the $error
flag for the field will be false
, even though the $invalid
flag is true
. For example, you will be required to call v$.firstName.value.$touch()
on blur
event when you edit the firstName
field.
Alternatively, you could use the $model
proxy object as the v-model
for the field. The $model
proxy is proxy copy of the validated value. So, v$.firstName.value.$model
is identical to form.firstName.value
. When you use $model
as the v-model
value for each validated field, Vuelidate
will automatically track the changes for those field and you don't have to call $touch()
when you make changes. For example, instead of v-model="form[key].value"
, you could use v-model="v$[key].value.$model"
to avoid calling v$.firstName.value.$touch()
.
The $autoDirty
property saves you all the stress by automatically track changes and marking each validated field as being dirty
when you change the field. It also ensures that you do not have to use the $model
proxy object. This is the strategy used for this tutorial.
Read more about the $dirty
state here from the docs.
More on the $lazy
property of validationConfig
By default validationConfig.$lazy
is set to false
. The effect of this property can make a big difference and show your deep understanding of how to validate forms and make them performant and user-friendly. Since $lazy
is set to false
by default, when a validated form is rendered, the $dirty
flag for reach validated field will be automatically set to true
. As discussed above, it results in the $error
flag being true
and error messages will be thrown for each errored field. So a user will immediately see validation errors and the field will be error state (showing red) even when they have not interacted with the form yet. This behaviour is not the best for user experience and could slow down the rendering of a large forms (with many fields).
However, when $lazy
is true
, errors will be thrown only when the field is set to $dirty
. That is, errors will only be shown when the user has interacted with each field and while the value of each field has not passed the validation contraints. This is a better user experience and performant for large forms.
As a bonus tip: When you are validating a field which will require a lot of user inputs such as a textarea
input or when you have an async validator (which could be fetching validation results from the database), you should be careful with the use of the global $autoDirty
and $lazy
properties. For the textarea
field, you can override the config an set $autoDirty
to false
and $lazy
to true
. For example, in user registration form:
const isUnique = async function(value) {// Check if email `value` exist on database}
....
email: {
required,
email,
isUnique: helpers.withMessage("This email is already taken.", isUnique),
$lazy: true,
$autoDirty: false
},
....
The above rule will ensure that the isUnique
async function is only called when you explicitly call v$.email.touch()
else, each key stroke will lead to a call to the isUnique
function. Additionally, you should set up a debounced
function to handle isUnique
function to reduce the number of calls made due to rapidly-emitted events.
So, on Line 254:
const v$ = useVuelidate(rules, form, { $lazy: true, $autoDirty: true });
We are have globally set both values to be true
so we will have tracking of changes across all validated fields and the form won't be automatically validated when rendered.
The submitForm
function
Before we are done with the setup
section, let's look at the submitForm function from Line 256. When the submitForm
function is called, the first thing we do is to call v$.value.$touch()
. Why? It is a good practice to call the $touch()
function on the root of the Vuelidate
instance i.e. v$
to make sure that all validated field on the form are set to $dirty
before will proceed with the rest of the form submission. Remember that when $lazy
is set to true
, validated fields will only emit errors when they have been interacted with. This means, you could end up submitting a form with some uncaught validation errors if you do not call v$.value.$touch()
on the root object.
Since we are using the composition API, the useValidate
function returns a Ref
. And the Ref
must be read with .value
, hence v$.value.$touch()
After setting the entire form to as dirty
, we also check that there is no error with v$.value.$error
. If there is an error, we call Quasar Notify
plugin to alert the user of the error. It is important to note that all errors in the each validated field is collected in the errors
property of the root object. So v$.value.$errors
contains all validation errors throughout the form. Each validated field will have its own $errors
property such as v$.value.firstName.$errors
. The $errors
property is an array of validation objects. Each validation object contains the following:
// Example for a `required` error on the `firstName.value` property
// v$.firstName.value.$errors
[{
$message: "First Name is required."
$params: Object
$pending: false
$property: "value"
$propertyPath: "firstName.value"
$response: false
$uid: "firstName.value-required"
$validator: "required"
}]
Open your Chrome DevTools, switch to Vue tab. On the top right, click
Select component in page
and click on theFirst Name
field. Within the components tree, click on theCreateContact
component. On the right-hand side of the Vue devtools, undersetup
, expandv$ > firstName > value > $errors
. If the$error
array is empty, go to the form, type something inside theFirst Name
and delete everything. This will trigger therequired
error for that field. Switch back to the Vue devtool and the$errors
property forv$.firstName.value
will be populated with a value similar to the above.
Try out the form by clicking the Submit
button with the form unfilled. Then scroll up to see the validation errors. Use the Vue devtool to inspect the errors and instructed above.
On Line 260, we length the length of the $errors
array and use that to compose the message for the error notification. If there is no error, a success notification is shown to the user.
On Line 280, we return the Vuelidate
instance, $v
, from the setup
function for consumption with the template
section.
The template
section: displaying the errors
Within the template
section, we add two props to the q-input
component to help in display the validation errors.
- The
error
prop notifies the component of the existence of the error and puts the component into an error state (which triggers red outlines, fills, and texts). We read from thev$[key].$error
(wherekey
is the string index of field object e.g.firstName
) flag to set the value of the prop. - The
error-message
prop sets the error string which will be displayed below the input field. We read from the$errors
array for each validated field by mapping the$message
property and calling theArray.join()
method to convert the array to a string separated by a new line ("\n"
).
Play around with form by entering values in the validated fields and see how the errors are displaying in the form.
Save all your files, commit and merge with the master branch.
git add .
git commit -m "feat(ui): complete validation for new contact form"
git push --set-upstream origin 04-validate-new-contact-creation-form
git checkout master
git merge master 04-validate-new-contact-creation-form
git push
In this next lesson, we will build out the table for displaying contacts on the home
page.