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))
    ;
  }

}

      

+11


source to share


5 answers


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 use switchMap

    orflatMap

  • You should consider using catchError in case the server call fails
  • angular docs: asynchronous validation
+17


source


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))
      );
    })
  )
}
      

Run codeHide result





* 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))
      );
    })
  )
}
      

Run codeHide result


+10


source


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!

+4


source


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);
  })
}

      

0


source


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))
        })

      

-2


source







All Articles