Free eBook: Build Your First Node App

How to Implement a Custom Validator Directive (Confirm Password) in Angular 2

In this article, we will be exploring Angular 2 built-in and custom validators.

Introduction

Angular 2 supports a few very useful native validators:

  1. required: validate if the field is mandatory
  2. minlength: validate the minimum length of the field
  3. maxlength: validate the maximum length of the field
  4. pattern: validate if the input value meets the defined pattern, e.g. email

We will build a form to capture user information based on this interface.

Table of Contents

    
    // user.interface.ts
    
    export interface User {
        username: string; // required, must be 5-8 characters
        email: string; // required, must be valid email format
        password: string; // required, value must be equal to confirm password.
        confirmPassword: string; // required, value must be equal to password.
    }

    Requirements

    Only show errors message for each field when field is dirty or form is submitted.

    Here is how the UI will look: Angular 2 Custom Validator Directive

    App Setup

    Here's our file structure:

    |- app/
        |- app.component.html
        |- app.component.ts
        |- app.module.ts
        |- equal-validator.directive.ts
        |- main.ts
        |- user.interface.ts
    |- index.html
    |- styles.css
    |- tsconfig.json

    In order to use new forms module, we need to npm install @angular/forms npm package and import the latest forms module in application module.

    $ npm install @angular/forms --save

    Here's the module for our application app.module.ts:

    // app.module.ts
    
    import { NgModule }      from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { FormsModule } from '@angular/forms';
    
    import { AppComponent }   from './app.component';
    
    @NgModule({
      imports:      [ BrowserModule, FormsModule ], // import forms module here
      declarations: [ AppComponent ],
      bootstrap:    [ AppComponent ],
    })
    
    export class AppModule { }

    The App Component

    Let's move on to create our app component.

    // app.component.ts
    
    import { Component, OnInit } from '@angular/core';
    import { User } from './user.interface';
    
    @Component({
      moduleId: module.id,
      selector: 'app-root',
      templateUrl: 'app.component.html',
      styleUrls: ['app.component.css']
    })
    export class AppComponent implements OnInit {
        public user: User;
    
        ngOnInit() {
            // initialize model here
            this.user = {
                username: '',
                email: '',
                password: '',
                confirmPassword: ''
            }
        }
    
        save(model: User, isValid: boolean) {
            // call API to save customer
            console.log(model, isValid);
        }
    }

    The HTML View

    This is how our HTML view will look.

    <!-- app.component.html -->
    
    <div>
        <h1>Add user</h1>
        <form #f="ngForm" novalidate (ngSubmit)="save(f.value, f.valid)">
            <!-- we will place our fields here -->
            <button type="submit" [disabled]="!myForm.valid">Submit</button>
        </form>
    </div>

    Implementation

    Let's add our controls one by one.

    Username

    Requirements: required, must be 5–8 characters

    <!-- app.component.html -->
    ...
    <div>
        <label>Username</label>
        <input type="text" name="username" [ngModel]="user.username" 
            required minlength="5" maxlength="8" #username="ngModel">
        <small [hidden]="username.valid || (username.pristine && !f.submitted)">
            Username is required (minimum 5 characters).
        </small>
    </div>
    <pre *ngIf="username.errors">{{ username.errors | json }}</pre>
    
    ...

    Since required, minlength, maxlength are built-in validators, it’s so easy to use them.

    We will only display the error message if username is not valid and the field is touched or form is submitted. The last line pre tag is very useful for debugging purposes during development. It displays all the validation errors of the field.

    Email

    Requirements: required, must be valid email format

    <!-- app.component.html -->
    ...
    <div>
        <label>Email</label>
        <input type="email" name="email" [ngModel]="user.email" 
            required pattern="^[a-zA-Z0–9_.+-]+@[a-zA-Z0–9-]+.[a-zA-Z0–9-.]+$" #email="ngModel" >
        <small [hidden]="email.valid || (email.pristine && !f.submitted)">
            Email is required and format should be <i>john@doe.com</i>.
        </small>
    </div>
    
    ...

    We set the email to required, then use the built-in pattern validator to test the value with email regex: ^[a-zA-Z0–9_.+-]+@[a-zA-Z0–9-]+.[a-zA-Z0–9-.]+$.

    Password and Confirm Password

    Requirements:

    1. Password: required, value must be equal to confirm password.
    2. Confirm password: required, value must be equal to password.
    <!-- app.component.html -->
    ...
    <div>
        <label>Password</label>
        <input type="password" name="password" [ngModel]="user.password" 
            required #password="ngModel">
        <small [hidden]="password.valid || (password.pristine && !f.submitted)">
            Password is required
        </small>
    </div>
    <div>
        <label>Retype password</label>
        <input type="password" name="confirmPassword" [ngModel]="user.confirmPassword" 
            required validateEqual="password" #confirmPassword="ngModel">
        <small [hidden]="confirmPassword.valid ||  (confirmPassword.pristine && !f.submitted)">
            Password mismatch
        </small>
    </div>
    ...

    validateEqual is our custom validator. It should validate the current input value against password input value.

    Custom confirm password validator

    We will develop a directive for validate equal.

    // equal-validator.directive.ts
    
    import { Directive, forwardRef, Attribute } from '@angular/core';
    import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
    @Directive({
        selector: '[validateEqual][formControlName],[validateEqual][formControl],[validateEqual][ngModel]',
        providers: [
            { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true }
        ]
    })
    export class EqualValidator implements Validator {
        constructor( @Attribute('validateEqual') public validateEqual: string) {}
    
        validate(c: AbstractControl): { [key: string]: any } {
            // self value (e.g. retype password)
            let v = c.value;
    
            // control value (e.g. password)
            let e = c.root.get(this.validateEqual);
    
            // value not equal
            if (e && v !== e.value) return {
                validateEqual: false
            }
            return null;
        }
    }

    The code is quite long, let’s break it down and look into it part by part.

    Directive declaration

    // equal-validator.directive.ts
    
    @Directive({
        selector: '[validateEqual][formControlName],[validateEqual] 
        [formControl],[validateEqual][ngModel]',
        providers: [
            { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true }
        ]
    })

    First, we define directive using the @Directive annotation. Then we specify the selector. Selector is mandatory. We will extend the built-in validators NG_VALIDATORS to use our equal validator in providers.

    Class definition

    // equal-validator.directive.ts
    
    export class EqualValidator implements Validator {
        constructor( @Attribute('validateEqual') public validateEqual: string) {}
    
        validate(c: AbstractControl): { [key: string]: any } {}
    }

    Our directive class should implement the Validator interface. Validator interface expecting a validate function. In our constructor, we inject the attribute value via annotation @Attribute(‘validateEqual’) and assign it to the validateEqual variable. In our example, the value of validateEqual would be “password”.

    Validate implementation

    // equal-validator.directive.ts
    
    validate(c: AbstractControl): { [key: string]: any } {
        // self value (e.g. retype password)
        let v = c.value;
    
        // control value (e.g. password)
        let e = c.root.get(this.validateEqual);
    
        // value not equal
        if (e && v !== e.value) return {
            validateEqual: false
        }
        return null;
    }

    First, we read the value of our input and assign it to v. Then, we find the password input control in our form and assign it to e. After that, we check for value equality, and return errors if it’s not equal.

    Import custom validator into our app module

    To use the custom validator in our form, we need to import it to our applcation module.

    // app.module.ts
    ...
    import { EqualValidator } from './equal-validator.directive';  // import validator
    import { AppComponent }   from './app.component';
    
    @NgModule({
      imports:      [ BrowserModule, FormsModule ],
      declarations: [ AppComponent, EqualValidator ], // import to app module
      bootstrap:    [ AppComponent ],
    })
    
    ...

    Tadaa! Let’s say you type “123” in the password field, then “xyz” in retype password, it should show you password mismatch error.

    Everything seems okay, but…

    Everything is working fine until you go and change the password value after you've entered text in the retype password field.

    For example, you type “123” in password field, then “123” in retype password, then change the password input to “1234”. The validation still passes. Why?

    It’s because we only apply our equal validator to retype password. It will trigger only when retype password value changes.

    Solution

    There are a few ways to fix this. We will discuss one of the solutions here. I'll leave it to you to figure out the others. We will reuse our validateEqual validator and add an attribute call reverse.

    <!-- app.component.html -->
    ...
    <input type="password" class="form-control" name="password" 
        [ngModel]="user.password" 
        required validateEqual="confirmPassword" reverse="true">
    
    <input type="password" class="form-control" name="confirmPassword"  
        [ngModel]="user.confirmPassword" 
        required validateEqual="password">
    
    ...
    • When reverse is false or not set, we will perform equal validation as explained in the previous section.
    • When reverse is true, we will still perform equal validation, but instead of adding errors to current control, we add errors to the target control.

    In our example, we set the password validation reverse to true. Whenever password is not equal to retype password, we will insert an error to confirm password field instead of reset password field.

    The complete custom validator code:

    // equal-validator.directive.ts
    
    import { Directive, forwardRef, Attribute } from '@angular/core';
    import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
    
    @Directive({
        selector: '[validateEqual][formControlName],[validateEqual][formControl],[validateEqual][ngModel]',
        providers: [
            { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true }
        ]
    })
    export class EqualValidator implements Validator {
        constructor(@Attribute('validateEqual') public validateEqual: string,
        @Attribute('reverse') public reverse: string) {
        }
    
        private get isReverse() {
            if (!this.reverse) return false;
            return this.reverse === 'true' ? true: false;
        }
    
        validate(c: AbstractControl): { [key: string]: any } {
            // self value
            let v = c.value;
    
            // control vlaue
            let e = c.root.get(this.validateEqual);
    
            // value not equal
            if (e && v !== e.value && !this.isReverse) {
                return {
                    validateEqual: false
                }
            }
    
            // value equal and reverse
            if (e && v === e.value && this.isReverse) {
                delete e.errors['validateEqual'];
                if (!Object.keys(e.errors).length) e.setErrors(null);
            }
    
            // value not equal and reverse
            if (e && v !== e.value && this.isReverse) {
                e.setErrors({ validateEqual: false });
            }
    
            return null;
        }
    }

    Summary

    There are other ways to solve the password and confirm password validation too. Some people suggest to add both password and confirm password in a group (stack overflow), then validate it.

    There's really no right or wrong, it’s up to you.

    More details:

    That's it. Happy coding!

    Jecelyn Yeen

    21 posts

    Coder. Diver. Board Game Lover.

    Speak English, Mandarin, JavaScript, Typescript, C# and more.

    GDE | Angular | Web Technologies.