• Angular
  • React
  • NextJs
  • Sass

Angular Async Validator in Template Driven Form

By Webrecto, September 18, 2023

In this article we will learn to create custom asynchronous validator and use it in our Angular template-driven form application. Angular provides AsyncValidator interface to create custom asynchronous validator. It has a validate() method that need to define. Use NG_ASYNC_VALIDATORS as provider to register custom async validators.

Here in our demo application, I will create custom async validators and validate a template-driven from.

AsyncValidator

Angular AsyncValidator interface is implemented by the classes that perform asynchronous validation. This contains validate() method that performs async validation against provided control. Find the validate() method declaration.

validate(control: AbstractControl<any, any>): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>

The parameter control is the control to validate against.
Return type is Promise or Observable that resolves a map of validation errors if validation fails, otherwise null.

Creating Async Validator

To create async validator for template-driven from, we need to create a Directive that will implement AsyncValidator interface and define validate method.

Find the code snippet to create a custom async validator to check existing user name.

existing-username-validator.ts

@Directive({
  selector: '[usernameExists]',
  standalone: true,
  providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ExistingUsernameValidatorDirective, multi: true }]
})
export class ExistingUsernameValidatorDirective implements AsyncValidator {
  constructor(private userService: UserService) { }
  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.userService.getUsernames(control.value).pipe(
      debounceTime(2000),
      map((usernames: string[]) => {
        return (usernames && usernames.length > 0) ? { "nameExists": true } : null;
      })
    );
  }
}

1. The selector usernameExists will be used as attribute in control.

<input name="username" usernameExists ngModel #usrname="ngModel">

2. The validate() method returns the observable of map of errors i.e. nameExists. Use it to display the error message.

<div *ngIf="usrname.errors['nameExists']">
    Username already exists.
</div>

3. The NG_ASYNC_VALIDATORS is an InjectionToken for registering additional asynchronous validators.

4. The debounceTime() is a RxJS operator that emits a notification from the source Observable only after the specified time has passed without another source emission.

5. Use standalone: true for standalone application.

Using with Standalone Application

1. Use standalone: true in async validator directive.

2. The standalone component needs to import all the custom async validators directives using imports attribute as following.

user.component.ts

@Component({
	selector: 'app-user',
	standalone: true,
	imports: [
		CommonModule,
		FormsModule,
		ExistingUsernameValidatorDirective,
		ExistingMobileNumberValidatorDirective
	],
        ------
})
export class UserComponent {}

Complete Example

In this example I am creating two async validators, one to check existing username and second to check existing mobile number. If username or mobile number already exists, then form validation will fail.

existing-username-validator.ts

import { Directive } from '@angular/core';
import { AsyncValidator, NG_ASYNC_VALIDATORS, AbstractControl, ValidationErrors } from '@angular/forms';
import { Observable } from "rxjs";
import { debounceTime, map } from "rxjs/operators";
import { UserService } from './user.service';

@Directive({
  selector: '[usernameExists]',
  standalone: true,
  providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ExistingUsernameValidatorDirective, multi: true }]
})
export class ExistingUsernameValidatorDirective implements AsyncValidator {
  constructor(private userService: UserService) { }
  validate(control: AbstractControl): Observable {
    return this.userService.getUsernames(control.value).pipe(
      debounceTime(1000),
      map((usernames: string[]) => {
        return (usernames && usernames.length > 0) ? { "nameExists": true } : null;
      })
    );
  }
}

existing-mobileno-validator.ts

import { Directive } from '@angular/core';
import { AsyncValidator, NG_ASYNC_VALIDATORS, AbstractControl, ValidationErrors } from '@angular/forms';
import { Observable } from "rxjs";
import { debounceTime, map } from "rxjs/operators";
import { UserService } from './user.service';

@Directive({
  selector: '[mobNumExists]',
  standalone: true,
  providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ExistingMobileNumberValidatorDirective, multi: true }]
})
export class ExistingMobileNumberValidatorDirective implements AsyncValidator {
  constructor(private userService: UserService) { }
  validate(control: AbstractControl): Observable {
    return this.userService.getMobileNumbers(control.value).pipe(
      debounceTime(1000),
      map(
        mobnum => {
          return (mobnum && mobnum.length > 0) ? { "mobExists": true } : null;
        }
      ));
  }
}

user.component.html

<h3>Angular Async Validator</h3>
<div *ngIf="formSubmitted && userForm.pristine" class="submitted"> Form submitted successfully. </div>
<form #userForm="ngForm" (ngSubmit)="onFormSubmit(userForm)">
  <div>Username:</div>
  <div>
    <input name="username" required usernameExists ngModel #usrname="ngModel">
    <div *ngIf="usrname.dirty && usrname.errors" class="error">
      <div *ngIf="usrname.errors['required']">
        Username required.
      </div>
      <div *ngIf="usrname.errors['nameExists']">
        Username already exists.
      </div>
    </div>
  </div>
  <br/><div>Mobile Number:</div>
  <div>
    <input name="mobileNumber" required mobNumExists ngModel #mobilenum="ngModel">
    <div *ngIf="mobilenum.dirty && mobilenum.errors" class="error">
      <div *ngIf="mobilenum.errors['required']">
        Mobile number required.
      </div>
      <div *ngIf="mobilenum.errors['mobExists']">
        Mobile number already exists.
      </div>
    </div>
  </div>
  <div>
    <br/><button [disabled]="!userForm.valid">Submit</button>
  </div>
</form>

user.component.ts

import { Component, OnInit } from '@angular/core';
import { NgForm, FormsModule } from '@angular/forms';
import { UserService } from './user.service';
import { CommonModule } from '@angular/common';
import { ExistingMobileNumberValidatorDirective } from './existing-mobileno-validator';
import { ExistingUsernameValidatorDirective } from './existing-username-validator';

@Component({
	selector: 'app-user',
	standalone: true,
	imports: [
		CommonModule,
		FormsModule,
		ExistingUsernameValidatorDirective,
		ExistingMobileNumberValidatorDirective
	],
	templateUrl: './user.component.html' 
})
export class UserComponent implements OnInit {
	formSubmitted = false;
	constructor(
		private userService: UserService) {
	}
	ngOnInit(): void {
	}
	onFormSubmit(userForm: NgForm) {
		this.userService.saveUser(userForm.value);
		this.formSubmitted = true;
		userForm.reset();
	}
}

user.service.ts

import { Injectable } from '@angular/core';
import { of } from 'rxjs';

@Injectable()
export class UserService {
	saveUser(data: any) {
		const user = JSON.stringify(data);
		console.log(user);
	}
	getUsernames(name: string) {
		if (name === "xyz") {
			return of(["xyz"]);
		} else {
			return of([]);
		}
	}
	getMobileNumbers(num: string) {
		if (num === "123456") {
			return of(["123456"]);
		} else {
			return of([]);
		}
	}
}

Find the print screen of the output.

Angular Async Validator in Template Driven Form

Reference

Download Source Code