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

Datetime:2016-08-23 02:34:56          Topic: AngularJS           Share

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

This article is built with Angular RC4. Angular RC5 is now out and requires some bootstrapping changes . We'll be updating this article accordingly.

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.

// 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:

App Setup

As of RC.2 - RC.4, deprecated forms is enabled by default.

Here's our file structure:

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

In order to use the new forms module, we need to npm install @angular/forms the package and enable it during application bootstrapping.

$ npm install @angular/forms --save

Here's the bootstrapping for our application in main.ts :

// main.ts

import { bootstrap } from '@angular/platform-browser-dynamic';
import { AppComponent } from './app/';
import { disableDeprecatedForms, provideForms } from '@angular/forms';

bootstrap(AppComponent, [
  disableDeprecatedForms(), // disable deprecated forms
  provideForms(), // enable new forms module
]);

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'],
  directives: [EqualValidator]
})
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, provide, 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.find(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.find(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 component

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

// app.component.ts

import { EqualValidator } from './equal-validator.directive';
@Component({
    moduleId: module.id,
    selector: 'app-root',
    templateUrl: 'app.component.html',
    styleUrls: ['app.component.css'],
    directives: [EqualValidator] // inject it here
})

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, provide, 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.find(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!





About List