Angular Async Validator Debounce Example
September 20, 2023
On this page, we will learn to create async validator with debounce in our Angular application. In async validation, request can be sent over HTTP to validate data. To create an async validator, we need to create a Directive that implements AsyncValidator
and override its validate()
method. This method returns Promise
or Observable
instance.
Debounce is removing unwanted input noise. To use async validator with debounce, we can use RxJS
debounceTime()
method. Here on this page, I will create a demo application with async validators that is using debounceTime()
method to debounce the unwanted request hits in validation.
RxJS debounceTime()
Find the RxJSdebounceTime()
time.
debounceTime(dueTime: number)
debounceTime
emits a notification from the source Observable
only after the specified time has passed without another source emission.
Find the code snippet.
fromEvent(document, 'click') .pipe(debounceTime(1000)) .subscribe(data => console.log("You clicked :" + data));
Creating Async Validator with debounceTime()
To create async validator, create aDirective
implementing AsyncValidator
interface and define its validate()
method.
@Directive({ selector: '[mobNumExists]', standalone: true, providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: ExistingMobileNumValidatorDirective, multi: true }] }) export class ExistingMobileNumValidatorDirective implements AsyncValidator { constructor(private userService: UserService) { } validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.userService.getMNumbers(control.value).pipe( debounceTime(1000), map(mobnum => { return (mobnum && mobnum.length > 0) ? { "mnumExists": true } : null; } )); } }
standalone: true
for standalone application. Debounce time is 1000 milli second in the above code. In this duration for multiple request, the above custom validation will emit only one time.
Complete Example
existing-usrname-validator.tsimport { 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: ExistingUsrnameValidatorDirective, multi: true }] }) export class ExistingUsrnameValidatorDirective implements AsyncValidator { constructor(private userService: UserService) { } validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.userService.getUsrNames(control.value).pipe( debounceTime(1000), map((usernames: string[]) => { return (usernames && usernames.length > 0) ? { "unameExists": true } : null; }) ); } }
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: ExistingMobileNumValidatorDirective, multi: true }] }) export class ExistingMobileNumValidatorDirective implements AsyncValidator { constructor(private userService: UserService) { } validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.userService.getMNumbers(control.value).pipe( debounceTime(1000), map(mobnum => { return (mobnum && mobnum.length > 0) ? { "mnumExists": true } : null; } )); } }
import { Component, OnInit } from '@angular/core'; import { NgForm, FormsModule } from '@angular/forms'; import { UserService } from './user.service'; import { CommonModule } from '@angular/common'; import { ExistingMobileNumValidatorDirective } from './existing-mobno-validator'; import { ExistingUsrnameValidatorDirective } from './existing-usrname-validator'; import { debounceTime, fromEvent } from 'rxjs'; @Component({ selector: 'app-user', standalone: true, imports: [ CommonModule, FormsModule, ExistingUsrnameValidatorDirective, ExistingMobileNumValidatorDirective ], templateUrl: './user.component.html' }) export class UserComponent implements OnInit { formSubmitted = false; constructor( private userService: UserService) { } ngOnInit(){ fromEvent(document, 'click') .pipe(debounceTime(1000)) .subscribe(data => console.log("You clicked :" + data)); } onFormSubmit(usrForm: NgForm) { this.userService.saveUser(usrForm.value); this.formSubmitted = true; usrForm.reset(); } }
<h3>Angular Async Validator with debounceTime</h3> <div *ngIf="formSubmitted && usrForm.pristine" class="submitted"> Form submitted successfully. </div> <form #usrForm="ngForm" (ngSubmit)="onFormSubmit(usrForm)"> <div>User name:</div> <div> <input name="username" required usernameExists ngModel #uname="ngModel"> <div *ngIf="uname?.errors?.['required']" class="error"> Username required. </div> <div *ngIf="uname?.errors?.['unameExists']" class="error"> User name already exists. </div> </div> <br /> <div>Mobile Number:</div> <div> <input name="mobileNumber" required mobNumExists ngModel #mobnum="ngModel"> <div *ngIf="mobnum?.errors?.['required']" class="error"> Mobile number required. </div> <div *ngIf="mobnum?.errors?.['mnumExists']" class="error"> Mobile number already registered. </div> </div> <div> <br /><button [disabled]="!usrForm.valid">Submit</button> </div> </form>
import { Injectable } from '@angular/core'; import { of } from 'rxjs'; @Injectable() export class UserService { saveUser(data: any) { console.log(data); } getUsrNames(uname: string) { if (uname === "Mohan") { return of(["Mohan"]); } else { return of([]); } } getMNumbers(mnum: string) { if (mnum === "222222") { return of(["222222"]); } else { return of([]); } } }
