Lesson 16

Advanced Form Validation

Creating advanced validations for the register form

PRO

Lesson Outline

Advanced Form Validation

The form we added to the register page uses Angular's FormBuilder to create a group of fields, and we've already set up some basic validations on those fields.

For the purpose of this lesson, I will assume that you are already familiar with creating forms with FormBuilder, but if you are not I would recommend taking a look at this blog post. The important thing to understand is that a FormGroup consists of FormControls.

A FormControl is tied to an input field, it has a value (i.e. the value the user has entered), and a validation state (i.e. whether or not the value is valid based on an optional validation function). Since each form control has a validation state, we can easily detect whether or not the form is valid, which allows us to do things like this:

if(this.registerForm.valid)

or this:

*ngIf="!registerForm.valid"

We can easily modify the interface or the behaviour of our application based on whether or not fields in the form are valid. In this lesson, we are going to cover the different kinds of validators we can use which will determine whether or not our form fields are valid.

There are primarily two types of validators: synchronous validators and asynchronous validators. Let's talk through the differences between those.

Synchronous Validators

A synchronous validator uses synchronous code to verify whether or not a field is valid. This means that the validation happens immediately, and it is generally the simpler of the two types of validators. We already have some synchronous validators defined in our register page, so let's take a look at those:

	username: ['', Validators.compose([Validators.maxLength(16), Validators.pattern('[a-zA-Z0-9]*'), Validators.required])],
	email: ['', Validators.compose([Validators.maxLength(30), Validators.required])],
	password: ['', Validators.compose([Validators.maxLength(30), Validators.required])],
	confirmPassword: ['', Validators.compose([Validators.maxLength(30), Validators.required])]

Synchronous validators are supplied as the second parameter (the fields value is the first parameter) - we don't have a third parameter just yet but we will in a moment when we add asynchronous validators. Let's take a look specifically at the validators for the username FormControl:

username: ['', Validators.compose([Validators.maxLength(16), Validators.pattern('[a-zA-Z0-9]*'), Validators.required])],

We are just using the default Validators that Angular provides, so we are using:

  • Validators.maxLength(16) to enforce that the maximum length of this field is 16 characters
  • Validators.pattern('[a-zA-Z0-9]*') to enforce a regex pattern on this field - this will only allow letters and numbers
  • Validators.required to enforce that this field is required

Since we have multiple validators we use Validators.compose to supply an array of all of the validators that we want to use for this form control. You can find other validators in the documentation, or you can also create your own custom validators (which we will be doing for our asynchronous validators in a moment).

If any of the conditions for these validators are not met, then the form control will be marked as invalid.

Asynchronous Validators

Asynchronous validators are basically identical in concept to synchronous validators, except for the fact that the validation runs asynchronously. This means that our application will continue executing other code whilst our validation function runs some asynchronous code, like a request to a server.

This is exactly what we are going to do now, we are going to write some custom asynchronous validators that will make a request to our server to verify usernames and emails (ensuring that they are not already in use).

Create an Email Validator

Creating our own custom validator is simple enough, we just need to create a function that takes in a FormControl (the field we are attempting to validate), and then return null if the validation passed, or an object indicating the error if it did not pass.

In the case of an asynchronous validator, we return a promise that resolves with either null or the object instead. Let's go ahead and create the email validator now and then talk through how it works.

Create a folder and file at src/app/validators/email.ts and add the following

import { Injectable } from '@angular/core';
import { FormControl } from '@angular/forms';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root',
})
export class EmailValidator {
  private debouncer: ReturnType<typeof setTimeout>;

  constructor(private authService: AuthService) {}

  async checkEmail(control: FormControl): Promise<null | Object> {
    clearTimeout(this.debouncer);

    return new Promise((resolve) => {
      this.debouncer = setTimeout(() => {
        this.authService.validateEmail(control.value).subscribe(
          (res: any) => {
            if (res.ok) {
              resolve(null);
            }
          },
          (err) => {
            resolve({ emailInUse: true });
          }
        );
      }, 1000);
    });
  }
}

In our case, we want to outsource the validation to our Auth service where we already have a function set up to validate an email. In order to be able to inject our Auth service into this validator, this validator class needs to be an @Injectable. In normal cases you don't need to set your custom validators up as services, they can just be a normal class that you import.

The checkEmail function mostly just conforms to what I've already explained. It sends the control's value to our validateEmail function and then resolves a promise with null if it succeeds, or it resolves it with:

{'emailInUse': true}

if it fails. However, there is a little bit of trickery going on here to add in debouncing. We don't want our server to get hammered with requests every time the user types a character in the input field - the validator will be triggered every time the value changes - so we add in a debouncer so that the validator will only run if the value has not changed for more than 1 second. This way, it should only trigger once the user has finished typing (or at least, it won't get triggered nearly as much).

Create a Username Validator

Now let's create our username validator, there's not going to be any surprises here - it's exactly the same as our email validator.

Create a file at src/app/validators/username.ts and add the following:

PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).