Tuesday, November 24, 2020

Custom Async Validator in Angular Template-Driven Form

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

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

Custom Asynchronous validator for Template-Driven form

If you want to write a custom async validator for a template-driven form that has to be written as an Angular directive which should 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.

Angular Custom validator Template-Driven form example

In this custom validator example we’ll create an Angular template-driven 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 in Angular

Directive as Async Validator (email.directive.ts)

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

@Directive({
  selector: '[uniqueemailvalidator]',
  providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: UniqueEmailValidatorDirective, multi: true}]
})
export class UniqueEmailValidatorDirective 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. Since it is a Directive that is specified by using the @Directive decorator.
  2. With in the providers array first element is provide: NG_ASYNC_VALIDATORS which registers the directive with the NG_ASYNC_VALIDATORS provider. That’s how Angular knows that this directive has to be used as an asynchronous validator.
  3. Second element useExisting: UniqueEmailValidatorDirective instructs that an existing instance of the directive has to be used rather than creating a new instance. if you use useClass: UniqueEmailValidatorDirective, then you’d be registering a new class instance.
  4. By using Third element multi: true you can register many initializers using the same token. In this context you can register a custom form validator using the built-in NG_ASYNC_VALIDATORS token.
  5. This directive is used as a custom async validator that means it has to be added as an attribute in a form control in the template. That attribute is added using the selector (uniqueemailvalidator ) specified here.
  6. Though you can write the logic for validation with in the validate method here but better to create a separate Angular Service class where you can add all of the required custom validators and then call them. Here it is done like that by creating a Service class and the service class in injected in the Directive in the constructor.
  7. 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}
  8. 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)

Since it is a template-driven form so not much is there in the Component class.

import { Component, ViewChild } from '@angular/core';
import { AbstractControl, FormControl, NgForm } from '@angular/forms';
import { Member } from './member.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  @ViewChild('membershipForm') memberForm : NgForm;
  membershiptypes = ['Silver', 'Gold', 'Platinum'];
  currentDate = new Date();
  member = new Member('', '', new Date(), '');
  submitted = false;

  onSubmit(){
    this.submitted = true;
    this.member.name = this.memberForm.value.name;
    this.member.mail = this.memberForm.value.email;
    this.member.membershipDate = this.memberForm.value.mdate;
    this.member.membershipType = this.memberForm.value.type; 
  }
}

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 (ngSubmit)="onSubmit()" #membershipForm="ngForm">
        <div class="form-group">
          <label for="name">Name</label>
          <input type="text" class="form-control" 
            id="name"
            ngModel name="name" #name="ngModel" required>     
            <div class="alert alert-danger" *ngIf="name.invalid && name.touched">Name is required</div>                  
        </div>
          
        <div class="form-group">
          <label for="email">email</label>
          <input type="email" class="form-control" 
              id="email" required email uniqueemailvalidator
              ngModel name="email" #email="ngModel"> 
              <div class="alert alert-danger" *ngIf="email.invalid && email.touched">
                <div *ngIf="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"
            [ngModel]="currentDate | date:'yyyy-MM-dd'"  name="mdate">                        
        </div>
        <div class="form-group">
          <label for="type">Membership Type</label>
          <select class="form-control" id="type"                    
              ngModel name="type" required #type="ngModel">
            <option *ngFor="let mtype of membershiptypes" [value]="mtype">{{mtype}}</option>
          </select>
          <div class="alert alert-danger" *ngIf="type.invalid && type.touched">
              Please select membership type
          </div> 
        </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>

In the control for email, ‘uniqueemailvalidator’ (selector from directive) is added to trigger the custom async validation. 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 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 */
}

In the index.html file, update the <head> tag to include the new style sheet.

<link rel="stylesheet" href="assets/forms.css">

Adding Directive to app module

You need to import and declare the created Directive in the app module.

import { AppComponent } from './app.component';
import { UniqueEmailValidatorDirective } from './email.directive';

@NgModule({
  declarations: [
    AppComponent,
    UniqueEmailValidatorDirective
  ],
  imports: [
  ...
  ...
})
export class AppModule { }
Async Validator in Angular Template-Driven Form

That's all for this topic Custom Async Validator in Angular Template-Driven 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 Template-Driven Form
  2. Custom Validator in Angular Reactive Form
  3. FormGroup in Angular With Examples
  4. FormBuilder in Angular Example
  5. FormArray in Angular With Example

You may also like-

  1. Angular @Input and @Output Example
  2. Angular Route Resolver - Passing Data Dynamically
  3. Angular ngSwitch Directive With Examples
  4. Angular Style Binding With Examples
  5. Private Methods in Java Interface
  6. Java Stream API Interview Questions And Answers
  7. BigInteger in Java With Examples
  8. @Required Annotation in Spring Framework

1 comment: