Monday, November 23, 2020

Custom Async Validator in Angular Reactive Form

In this tutorial we’ll see how to create a custom asynchronous validator to be used with Angular Reactive form.

For custom async validator in Template-Driven form refer this post- Custom Async Validator in Angular Template-Driven Form


Types of Validator functions

Validator functions can be either synchronous or asynchronous.

  • Sync validators: Synchronous validator functions are passed a FormControl instance as argument and immediately return either a set of validation errors or null. You can pass these in as the second argument when you instantiate a FormControl.
  • Async validators: Asynchronous validator functions are passed a FormControl instance as argument and return a Promise or Observable that later emits a set of validation errors or null. You can pass these in as the third argument when you instantiate a FormControl.

Asynchronous validator in Angular

Asynchronous validators implement the AsyncValidator interface.

interface AsyncValidator extends Validator {
  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>

  // inherited from forms/Validator
  validate(control: AbstractControl): ValidationErrors | null
  registerOnValidatorChange(fn: () => void)?: void
}

Your Async validator class has to implement the validate() function which must return a Promise or an observable.

Note that asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful. This precedence of synchronous validation helps in avoiding potentially expensive async validation processes (such as an HTTP request) if the more basic validation methods have already found invalid input.

Custom Async validator in Angular Recative form Example

In this custom Async validator example we’ll create an Angular reactive form to capture membership details which has a field ‘email’. We want to validate that the entered email is not already taken. To check that we’ll write a custom async validator.

Initial form state-

Async validator angular

Async Validator (email.validator.ts)

The following code creates the validator class, MyEmailValidator, which implements the AsyncValidator interface.

import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { EmailService } from './services/email.service';

@Injectable({ providedIn: 'root' })
export class MyEmailValidator implements AsyncValidator{
  constructor(private emailService: EmailService) {}
  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.emailService.isEmailTaken(control.value).pipe(
      map(isEmailTaken => (isEmailTaken ? {emailTaken: true} : null)),
      catchError(()=> of(null))
    );
  }
}

Some important points to note here-

  1. Validator class implements AsyncValidator interface.
  2. validate() method implementation returns either a Promise or an observable that resolves a map of validation errors if validation fails, otherwise null.
  3. Actual task of checking of the entered email already exists or not is delegated to a Service class which returns Observable<boolean> as the result.
  4. The validate() method pipes the response through the map operator and transforms it into a validation result which is a map of validation errors having key, value pair if validation fails otherwise null. In our case that key, value pair is {emailTaken: true}
  5. Any potential errors are handles using the catchError operator, in which case null is returned meaning no validation errors. You could handle the error differently and return the ValidationError object instead.

Service class (email.service.ts)

import { Injectable } from '@angular/core';
import { Observable, of} from 'rxjs';
import { delay } from 'rxjs/operators';
const EMAILS = ['test@test.com', 'user@test.com']
@Injectable({ providedIn: 'root' })
export class EmailService{
  isEmailTaken(email: string): Observable<boolean> {
    const isTaken = EMAILS.includes(email);

    return of(isTaken).pipe(delay(500));
  }
}

Some important points to note here-

  1. This service class simulates the call to API by using rxjs method of and operator delay (1/2 second delay here) rather than a real http request.
  2. There is an array of emails, entered email is checked against this array to see if array already includes the entered email returning true if it does otherwise false.

Data model (member.model.ts)

Member class defines the data model reflected in the form, we’ll bind values from the form to an object of this Member class.

export class Member {
  name: string;
  mail: string;
  membershipDate: Date;
  membershipType: string;
  constructor(name: string, mail: string, membershipDate: Date, membershipType: string) {
    this.name = name;
    this.mail = mail;
    this.membershipDate  = membershipDate;
    this.membershipType = membershipType;
  }
}

Component class (app.compnent.ts)

import { DatePipe } from '@angular/common';
import { Component, OnInit} from '@angular/core';
import { EmailValidator, FormControl, FormGroup, Validators} from '@angular/forms';
import { MyEmailValidator } from './email.validator';
import { Member } from './member.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html', 
  providers: [DatePipe]
})
export class AppComponent implements OnInit {
  membershiptypes = ['Silver', 'Gold', 'Platinum'];
  currentDate = new Date();
  member = new Member('', '', new Date(), '');
  submitted = false;
  membershipForm: FormGroup;
  constructor(private datePipe: DatePipe, private myEmailValidator: MyEmailValidator){ }
  ngOnInit() {
    this.membershipForm = new FormGroup({
      memberName: new FormControl(null, [Validators.required, Validators.minLength(5)]),
      email: new FormControl(null, [Validators.required, Validators.email], this.myEmailValidator.validate.bind(this.myEmailValidator)),    
      mdate: new FormControl(this.datePipe.transform(this.currentDate, 'yyyy-MM-dd')),
      membershipType: new FormControl('Silver')
    });
  }
  onSubmit(){
    this.submitted = true;
    this.member.name = this.membershipForm.value.memberName;
    this.member.mail = this.membershipForm.value.email;
    this.member.membershipDate = this.membershipForm.value.mdate;
    this.member.membershipType = this.membershipForm.value.membershipType; 
  }
}

In the component class Async validator class in injected in the constructor. In the FormControl instance for the email Validator function of the custom async validator is bound as the third argument. As the second argument bulit-in validators required and email are passed.

 email: new FormControl(null, [Validators.required, Validators.email], this.myEmailValidator.validate.bind(this.myEmailValidator))

Template (app.component.html)

<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8">
      <h1>Membership Form</h1>
      <form [formGroup]="membershipForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="memberName">Name</label>
          <input type="text" class="form-control" id="memberName"
            formControlName="memberName">       
          <div class="alert alert-danger" *ngIf="membershipForm.get('memberName').invalid 
            && membershipForm.get('memberName').touched">
            <div *ngIf="membershipForm.get('memberName').errors.required">
              Name is required.
            </div>
            <div *ngIf="membershipForm.get('memberName').errors.minlength">
              Name must be at least 5 characters long.
            </div>
          </div>                      
        </div>
        <div class="form-group">
          <label for="email">email</label>
          <input type="email" class="form-control" id="email"
          formControlName="email">  
           <div *ngIf="membershipForm.get('email').pending">Validating...</div>

          <div class="alert alert-danger" *ngIf="membershipForm.get('email').invalid 
              && membershipForm.get('email').touched">
              <div *ngIf="membershipForm.get('email').errors?.emailTaken; else elseBlock">
                Email is already taken.
              </div>
              <ng-template #elseBlock>
                Please enter a valid email
              </ng-template>
                  
          </div>                   
        </div>
        <div class="form-group">
          <label for="mdate">Membership Date</label>
          <input type="date" class="form-control" id="mdate"
          formControlName="mdate">                        
        </div>
        <div class="form-group">
          <label for="type">Membership Type</label>
          <select class="form-control" id="type"                    
          formControlName="membershipType">
            <option *ngFor="let mtype of membershiptypes" [value]="mtype">{{mtype}}</option>
          </select>
        </div>
        <button type="submit" [disabled]="membershipForm.invalid" class="btn btn-success">Submit</button>
      </form> 
    </div>
  </div>
  <hr>
  <div *ngIf="submitted">
    <div class="row">
      <div class="col-xs-12 col-sm-10 col-md-8">
        <p>Name: {{member.name}}</p>
        <p>email: {{member.mail}}</p>
        <p>Membership Date: {{member.membershipDate | date:'dd/MM/yyyy'}}</p>
        <p>Membership Type: {{member.membershipType}}</p>
      </div>
    </div>
  </div>  
</div>

When using async validator with a FormControl, the control is marked as pending and remains in this state until the observable chain returned from the validate() method completes.

Following if condition uses that state to display ‘Validating...’ message.

 <div *ngIf="membershipForm.get('email').pending">Validating...</div> 

If there is a validation error, ValidationErrors map would be returned. There is a check using the passed key ‘emailTaken’ and a message is displayed if such key exists.

There is also an else block with ngIf for all the other errors that may render this control invalid.

CSS class (src/assets/forms.css)

You will also need to add a CSS class to show the bars on the left side of the controls.

.ng-invalid.ng-untouched:not(form)  {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid.ng-touched:not(form)  {
  border-left: 5px solid #a94442; /* red */
}
custom async validator angular reactive form

That's all for this topic Custom Async Validator in Angular Reactive Form. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Angular Tutorial Page


Related Topics

  1. Custom Validator in Angular Reactive Form
  2. Custom Validator in Angular Template-Driven Form
  3. Radio Button in Angular Form Example
  4. Angular Form setValue() and patchValue()
  5. FormArray in Angular With Example

You may also like-

  1. Injector Hierarchy and Service Instances in Angular
  2. Angular CanActivateChild Guard to protect Child Routes
  3. Angular Event Binding With Examples
  4. How to Add Bootstrap to Angular Application
  5. Creating a Maven Project in Eclipse
  6. Java Abstract Class and Abstract Method
  7. Count Number of Times Each Character Appears in a String Java Program
  8. Spring Thread Pooling Support Using TaskExecutor

No comments:

Post a Comment