Validating the Contact Form with Vuelidate | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)

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

  1. 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
    
  2. 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.

  3. 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.

  1. Open ui/quasar.conf.js. We will add the Notify plugin for in-app notifications. Update the framework > config property by adding notify: { position: "top" }. And update framework > plugins to plugins: ["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.

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:

  1. The rules,
  2. The form schema,
  3. A validationConfig object. Details about the validationConfig 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 the First Name field. Within the components tree, click on the CreateContact component. On the right-hand side of the Vue devtools, under setup, expand v$ > firstName > value > $errors. If the $error array is empty, go to the form, type something inside the First Name and delete everything. This will trigger the required error for that field. Switch back to the Vue devtool and the $errors property for v$.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.

  1. 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 the v$[key].$error(where key is the string index of field object e.g. firstName) flag to set the value of the prop.
  2. 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 the Array.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.