Angular Async Validator in Template Driven Form
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.