I'm using custom async validator with Angular 4 reactive forms to check if E-Mail address is already taken by calling a backend.
However, Angular calls the validator, which makes request to the server for every entered character. This creates an unnecessary stress on the server.
Is it possible to elegantly debounce async calls using RxJS observable?
import {Observable} from 'rxjs/Observable';
import {AbstractControl, ValidationErrors} from '@angular/forms';
import {Injectable} from '@angular/core';
import {UsersRepository} from '../repositories/users.repository';
@Injectable()
export class DuplicateEmailValidator {
constructor (private usersRepository: UsersRepository) {
}
validate (control: AbstractControl): Observable<ValidationErrors> {
const email = control.value;
return this.usersRepository
.emailExists(email)
.map(result => (result ? { duplicateEmail: true } : null))
;
}
}
While @Slava's answer is right. It is easier with Observable :
return (control: AbstractControl): Observable<ValidationErrors> => {
return Observable.timer(this.debounceTime).switchMap(()=>{
return this.usersRepository
.emailExists(control.value)
.map(result => (result ? { duplicateEmail: true } : null));
});
}
updated with modern RxJS:
return (control: AbstractControl): Observable<ValidationErrors> => {
return timer(this.debounceTime).pipe(
switchMap(()=>this.usersRepository.emailExists(control.value)),
map(result => (result ? { duplicateEmail: true } : null))
);
}
Notes:
Observable
timer
emits only one value it does not matter if we use switchMap
or flatMap
UPDATE RxJS 6.0.0:
import {of, timer} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
return (control: AbstractControl): Observable<ValidationErrors> => {
return timer(500).pipe(
switchMap(() => {
if (!control.value) {
return of(null)
}
return this.usersRepository.emailExists(control.value).pipe(
map(result => (result ? { duplicateEmail: true } : null))
);
})
)
}
*RxJS 5.5.0
For everyone who is using RxJS ^5.5.0 for better tree shaking and pipeable operators
import {of} from 'rxjs/observable/of';
import {map, switchMap} from 'rxjs/operators';
import {TimerObservable} from 'rxjs/observable/TimerObservable';
return (control: AbstractControl): Observable<ValidationErrors> => {
return TimerObservable(500).pipe(
switchMap(() => {
if (!control.value) {
return of(null)
}
return this.usersRepository.emailExists(control.value).pipe(
map(result => (result ? { duplicateEmail: true } : null))
);
})
)
}
After studying some offered solutions with Observables I found them too complex and decided to use a solution with promises and timeouts. Although blunt, this solution is much simpler to comprehend:
import 'rxjs/add/operator/toPromise';
import {AbstractControl, ValidationErrors} from '@angular/forms';
import {Injectable} from '@angular/core';
import {UsersRepository} from '../repositories/users.repository';
@Injectable()
export class DuplicateEmailValidatorFactory {
debounceTime = 500;
constructor (private usersRepository: UsersRepository) {
}
create () {
let timer;
return (control: AbstractControl): Promise<ValidationErrors> => {
const email = control.value;
if (timer) {
clearTimeout(timer);
}
return new Promise(resolve => {
timer = setTimeout(() => {
return this.usersRepository
.emailExists(email)
.map(result => (result ? { duplicateEmail: true } : null))
.toPromise()
.then(resolve)
;
}, this.debounceTime);
});
}
}
}
Here, I'm converting existing observable to promise using toPromise()
operator of RxJS. Factory function is used because we need a separate timer for each control.
Please consider this a workaround. Other solutions, which actually use RxJS, are most welcome!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With