How to reverse an asynchronous validator in Angular 4 with RxJS observable?
I am using a custom asynchronous validator with Angular 4 reactive forms to check if an email has already been submitted by calling the backend.
However, Angular calls a validator that makes a request to the server for every character entered. This creates unnecessary stress on the server.
Is it possible to gracefully discard asynchronous calls with an 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)) ; } }
source to share
So far @ Slava's answer is correct. It's 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));
});
}
Notes:
- Angular automatically unsubscribes from the returned one
Observable
- timer () with one argument will return only one item
- since it
timer
only produces one value, it doesn't matter if we useswitchMap
orflatMap
- You should consider using catchError in case the server call fails
- angular docs: asynchronous validation
source to share
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 anyone using RxJS ^ 5.5.0 for better tree handling
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))
);
})
)
}
source to share
After researching some of the suggested solutions with Observables, I found them too complicated and decided to use the solution with promises and timeouts. While it's dumb, this solution is much easier to understand:
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 am converting existing observables to promises using toPromise()
the RxJS operator. The Factory function is used because each control needs a separate timer.
Please think about this in a workaround. Other solutions that actually use RxJS are welcome!
source to share
I think your method is delaying, not debounce, and then finds a sample way to archive that result.
import { debounce } from 'lodash';
...
constructor() {
this.debounceValidate = debounce(this.debounceValidate.bind(this), 1000);
}
debounceValidate(control, resolve) {
...//your validator
}
validate (control: AbstractControl): Promise {
return new Promise(resolve => {
this.debounceValidate(control, resolve);
})
}
source to share
If you want to implement it using RxJs, you can listen for ValueChanges values ββexplicitly and apply an asynchronous validator on it. For example, if you have a ref to your abstractControl, you can do,
ref.valueChanges.debounceTime(500).subscribe(//value is new value of control
value=>{this.duplicateValidator.validate(value)//duplicateValidator is ref to validator
.then(d => console.log(d))
.catch(d=>console.log(d))
})
source to share