Как отменить асинхронный валидатор в Angular 4 с RxJS наблюдаемым?

Я использую собственный асинхронный валидатор с Angular 4 реактивными формами, чтобы проверить, был ли уже отправлен адрес электронной почты, вызвав бэкэнд.

Однако Angular вызывает валидатор, который делает запрос на сервер для каждого введенного символа. Это создает ненужный стресс на сервере.

Можно ли элегантно отказаться от асинхронных вызовов с помощью RxJS наблюдаемого?

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

}

Ответ 1

Пока @Slava ответ правильный. Это проще с Observable:

return (control: AbstractControl): Observable<ValidationErrors> => {
      return Observable.timer(this.debounceTime).switchMap(()=>{
        return this.usersRepository
            .emailExists(control.value)
            .map(result => (result ? { duplicateEmail: true } : null));
      });
}

Заметки:

  • Angular автоматически отписывается от возвращаемого Observable
  • timer() с одним аргументом выдаст только один элемент
  • так как timer выдает только одно значение, не имеет значения, используем ли мы switchMap или flatMap
  • Вы должны рассмотреть возможность использования catchError в случае сбоя вызова сервера
  • угловые документы: асинхронная проверка

Ответ 2

ОБНОВЛЕНИЕ 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))
      );
    })
  )
}

Ответ 3

Изучив некоторые предлагаемые решения с Observables, я нашел их слишком сложными и решил использовать решение с promises и таймаутами. Хотя это тупое, это решение гораздо проще понять:

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

    }

  }

}

Здесь я конвертирую существующие наблюдаемые в обещания, используя оператор toPromise() оператора RxJS. Функция Factory используется, потому что для каждого элемента управления нужен отдельный таймер.


Пожалуйста, подумайте об этом обходным путем. Другие решения, которые на самом деле используют RxJS, приветствуются!

Ответ 4

Я думаю, что ваш метод задерживает, а не debounce, а затем находит образец способа для архивирования этого результата.

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

Ответ 5

Если вы хотите реализовать его с помощью RxJs, вы можете прослушивать значения ValueChanges явно и применять на нем асинхронный валидатор. Например, если у вас есть ссылка ref на ваш abstractControl, вы можете сделать,

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