Tutorial

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

Draft updated on Invalid Date
author

Jecelyn Yeen

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

This tutorial is out of date and no longer maintained.

Introduction

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

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

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

Here is how the UI will look:

Angular 2 Custom Validator Directive

View Angular 2 - Custom Validator Directive (final) scotch on plnkr

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 the new forms module, we need to npm install @angular/forms npm package and import the latest forms module in the 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 the 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 application 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 the password, it should show you a 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 the password field, then “123” in the retype password field, then change the password input to “1234”. The validation still passes. Why?

It’s because we only apply our equal validator to retype the password. It will trigger only when the 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 the password field instead of reseting 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;
    }
}

Conclusion

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!

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Jecelyn Yeen

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.