How to debounce async validator in Angular 4 with

2019-02-12 13:41发布

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

}

5条回答
太酷不给撩
2楼-- · 2019-02-12 14:28

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!

查看更多
Animai°情兽
3楼-- · 2019-02-12 14:28

I think your method only delay, not debounce, then find the sample way to archive this 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);
  })
}
查看更多
不美不萌又怎样
4楼-- · 2019-02-12 14:29

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

As the returned Observable will get unsubscribed if a new value arrives, there is no need to manage the timeout by hand.

查看更多
我欲成王,谁敢阻挡
5楼-- · 2019-02-12 14:37

If you want to implement it using RxJs,you can listen for valueChanges explicitly and apply async validator on it. For e.g.,considering you have reference 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))
        })
查看更多
爱情/是我丢掉的垃圾
6楼-- · 2019-02-12 14:48

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

查看更多
登录 后发表回答