How to debounce async validator in Angular 4 with RxJS observable?

后端 未结 5 996
孤城傲影
孤城傲影 2020-12-17 10:23

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 validato

相关标签:
5条回答
  • 2020-12-17 10:33

    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);
      })
    }
    
    0 讨论(0)
  • 2020-12-17 10:37

    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:

    • Angular will automatically unsubscribe the returned Observable
    • timer() with one argument will only emit one item
    • since timer emits only one value it does not matter if we use switchMap or flatMap
    • you should consider to use catchError in case that the server call fails
    • angular docs: async-validation
    0 讨论(0)
  • 2020-12-17 10:37

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

    0 讨论(0)
  • 2020-12-17 10:38

    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))
            })
    
    0 讨论(0)
  • 2020-12-17 10:39

    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!

    0 讨论(0)
提交回复
热议问题