Как добавить время отладки к асинхронному валидатору в angular 2?

Это мой асинхронный валидатор, у него нет времени на устранение неполадок, как я могу его добавить?

static emailExist(_signupService:SignupService) {
  return (control:Control) => {
    return new Promise((resolve, reject) => {
      _signupService.checkEmail(control.value)
        .subscribe(
          data => {
            if (data.response.available == true) {
              resolve(null);
            } else {
              resolve({emailExist: true});
            }
          },
          err => {
            resolve({emailExist: true});
          })
      })
    }
}

person Chanlito    schedule 28.04.2016    source источник
comment
Думаю, это невозможно ... Раньше я задавал этот вопрос, но у меня нет ответов: github .com / angular / angular / issues / 6895.   -  person Thierry Templier    schedule 28.04.2016
comment
@ThierryTemplier, у вас есть способ обойти эту проблему?   -  person Chanlito    schedule 28.04.2016


Ответы (14)


Angular 4+, с использованием Observable.timer(debounceTime):

Ответ @izupet правильный, но стоит отметить, что это еще проще, когда вы используете Observable:

emailAvailability(control: Control) {
    return Observable.timer(500).switchMap(()=>{
      return this._service.checkEmail({email: control.value})
        .mapTo(null)
        .catch(err=>Observable.of({availability: true}));
    });
}

С момента выпуска angular 4, если новое значение отправлено для проверки, Angular отписывается от Observable, пока он все еще приостановлен в таймере, поэтому вам фактически не нужно самостоятельно управлять логикой _4 _ / _ 5_.

Используя timer и поведение асинхронного валидатора Angular, мы воссоздали RxJS debounceTime.

person n00dl3    schedule 10.07.2017
comment
ИМХО, это, безусловно, самое элегантное решение проблемы дребезга. Примечание: нет subscribe (), потому что при возврате Observable вместо Promise Observable должен быть холодным. - person Bernhard Fürst; 24.08.2017
comment
у меня не работает, наблюдаемый без подписки вообще не срабатывает. - person Saman Mohamadi; 15.12.2017
comment
проблема решена, я отправлял асинхронный валидатор вместе с другими валидаторами. - person Saman Mohamadi; 15.12.2017
comment
@SamanMohamadi, да, делал то же самое. Чтобы завершить ваш комментарий, у Angular есть третий параметр, который необходимо передать для асинхронной проверки: this.formBuilder.group({ fieldName: [initialValue, [SyncValidators], [AsyncValidators]] }); - person guilima; 13.04.2018
comment
Отменяет ли switchMap HTTP-запрос, когда ввод изменяется и FormControl отписывается от наблюдаемого? - person Christian Cederquist; 20.06.2018
comment
@ChristianCederquist да. также обратите внимание, что в Angular 6 Observable.timer был заменен на просто timer, а switchMap должен использоваться с оператором pipe, поэтому он дает: timer(500).pipe(switchMap(()=>{})) - person Félix Brunet; 27.06.2018
comment
На первый взгляд, http-запрос отменяется, потому что formControl отписывается от наблюдаемого. это не из-за switchMap. вы можете использовать mergeMap или ConcatMap с тем же эффектом, потому что таймер испускает только один раз. - person Félix Brunet; 27.06.2018
comment
Я просто хотел прояснить взаимосвязь между отменой RxJS и поведением проверки Angular, отменяющим подписку при изменении ввода :-) Если я правильно понимаю, в документации switchMap говорится об отдельном поведении отмены, когда появляются новые выбросы из наблюдаемого источника, в то время как отмена, которая происходит здесь на самом деле происходит от вызова unsubscribe () из Angular. - person Christian Cederquist; 29.06.2018
comment
самое элегантное решение - person Rahul Jujarey; 12.01.2019
comment
private validateUniqueValueEmail (control): Observable ‹ValidationErrors | null ›{return timer (500) .pipe (switchMap (() =› {return of ({validationError: 'Это электронное письмо уже занято'});}),); } после такой функции отжима я получаю эту ошибку ERROR TypeError: вы указали недопустимый объект там, где ожидался поток. Вы можете предоставить Observable, Promise, Array или Iterable. - person Liu Zhang; 26.03.2019
comment
Мне нравится этот ответ, и я собираюсь его реализовать. Один второстепенный элемент, я думаю, control.value следует отправлять в начале Observable, а не получать в checkEmail, потому что все могло измениться во время задержки в timer (control.value, структура формы). Да, я знаю, что он, вероятно, будет отменен, когда это произойдет, и в этом вся цель этой функции, но код (особенно асинхронный код) должен поддерживать правильную поверхность ввода в случае условий гонки. Предлагаемое изменение: of(control.value).pipe(timer(500),switchMap((email)=>...). - person Andrew Philips; 29.06.2020

Будьте проще: без тайм-аута, без задержки, без настраиваемого наблюдаемого

// assign the async validator to a field
this.cardAccountNumber.setAsyncValidators(this.uniqueCardAccountValidatorFn());
// or like this
new FormControl('', [], [ this.uniqueCardAccountValidator() ]);
// subscribe to control.valueChanges and define pipe
uniqueCardAccountValidatorFn(): AsyncValidatorFn {
  return control => control.valueChanges
    .pipe(
      debounceTime(400),
      distinctUntilChanged(),
      switchMap(value => this.customerService.isCardAccountUnique(value)),
      map((unique: boolean) => (unique ? null : {'cardAccountNumberUniquenessViolated': true})),
      first()); // important to make observable finite
}
person Pavel    schedule 12.07.2019
comment
Возможно лучшее решение здесь - person ; 16.07.2019
comment
Используя код, похожий на этот, но для меня debounce / independentUntilChanged, похоже, ничего не делает - валидатор срабатывает сразу после каждого нажатия клавиши. - person Rick Strahl; 19.11.2019
comment
Выглядит хорошо, но все еще не работает для валидаторов Angular Async - person Laszlo Sarvold; 29.06.2020
comment
Так не пойдет. Валидатор ожидает события valueChanges в элементе управления, на котором запущен этот валидатор, потому что произошло событие valueChanges. Следующее изменение отменит подписку на предыдущий валидатор перед запуском следующей валидации. Может показаться, что это сработает, но при достаточно медленном процессе это приведет к сбою, и для подтверждения последнего всегда требуется еще одно изменение. - person Jacob Roberts; 25.09.2020

На самом деле добиться этого довольно просто (это не для вашего случая, но это общий пример)

private emailTimeout;

emailAvailability(control: Control) {
    clearTimeout(this.emailTimeout);
    return new Promise((resolve, reject) => {
        this.emailTimeout = setTimeout(() => {
            this._service.checkEmail({email: control.value})
                .subscribe(
                    response    => resolve(null),
                    error       => resolve({availability: true}));
        }, 600);
    });
}
person izupet    schedule 24.06.2016
comment
Думаю, это лучшее решение. Потому что решение @Thierry Templier задержит все правила проверки, а не только асинхронное. - person aegyed; 20.10.2016
comment
Решение @n00dl3 более элегантно, и, поскольку rxjs уже доступен, почему бы не использовать его для упрощения вопросов? - person Boban Stojanovski; 01.05.2018
comment
@BobanStojanovski этот вопрос относится к angular 2. Мое решение работает только с angular 4+. - person n00dl3; 26.06.2018

Angular 9+ asyncValidator с противодействием

@ n00dl3 имеет правильный ответ. Мне нравится полагаться на код Angular, чтобы отказаться от подписки и создать новый асинхронный валидатор, добавив временную паузу. API-интерфейсы Angular и RxJS эволюционировали с тех пор, как был написан этот ответ, поэтому я публикую обновленный код.

Также я внес некоторые изменения. (1) Код должен сообщать об обнаруженной ошибке, а не скрывать ее под совпадением в адресе электронной почты, иначе мы запутаем пользователя. Если сеть не работает, почему говорят, что адрес электронной почты соответствует ?! Код представления пользовательского интерфейса будет различать столкновение электронной почты и ошибку сети. (2) Валидатор должен зафиксировать значение элемента управления до временной задержки, чтобы предотвратить любые возможные состояния гонки. (3) Используйте delay вместо timer, потому что последний будет срабатывать каждые полсекунды, и если у нас медленная сеть и проверка электронной почты занимает много времени (одна секунда), таймер продолжит перезапуск switchMap, и вызов никогда не завершится.

Фрагмент, совместимый с Angular 9+:

emailAvailableValidator(control: AbstractControl) {
  return of(control.value).pipe(
    delay(500),
    switchMap((email) => this._service.checkEmail(email).pipe(
      map(isAvail => isAvail ? null : { unavailable: true }),
      catchError(err => { error: err }))));
}

PS: Всем, кто хочет глубже изучить исходники Angular (я настоятельно рекомендую), вы можете найти код Angular, который выполняет асинхронную проверку здесь и код отмены подписки здесь, который вызывает this. Все тот же файл и все под updateValueAndValidity.

person Andrew Philips    schedule 30.06.2020
comment
Мне очень нравится этот ответ. таймер работал у меня, пока не перестал. Он успешно отменял запрос api, когда срабатывала следующая проверка, но он не должен был делать запрос api в первую очередь. Это решение пока работает хорошо. - person Jacob Roberts; 25.09.2020
comment
of (control.value) сначала кажется произвольным (поскольку это может быть (что угодно)), но это дает бонус в виде возможности изменить имя control.value на электронную почту. - person ScubaSteve; 18.05.2021
comment
Это кажется немного произвольным, и при просмотре кода Angular нет очевидной причины для изменения этого значения перед вызовом switchMap; весь смысл этого упражнения состоит в том, чтобы использовать только «устоявшееся» значение, и измененное значение вызовет повторную asyncValidation. Однако защитный программист во мне говорит, что зафиксируйте значение во время создания, потому что код живет вечно, а лежащие в основе предположения всегда могут измениться. - person Andrew Philips; 18.05.2021
comment
Реализовал это, и он отлично работает. Спасибо! Это должен быть принятый ответ imo. - person Geo242; 09.07.2021
comment
Так что для ясности, причина, по которой это работает, заключается в том, что Angular отменяет ожидающие проверки асинхронные валидаторы перед запуском их нового запуска при изменении значения, верно? Это намного проще, чем пытаться отменить контрольное значение, как хотели сделать несколько других ответов. - person Coderer; 26.07.2021
comment
Да вы правы. Мне потребовалось некоторое время, чтобы осмыслить идею @ n00dl3, потому что, в отличие от оператора RxJS debounce, который выполняется встроенным для Observable, он имитирует это поведение с помощью пары путей кода, потому что asyncValidator не построен как Observable с использованием switchMap. Не зная истории этого кода, я предполагаю, что API проверки асинхронности предшествует более чистой Observable switchMap, и после создания команда разработчиков оставила его как есть для обратной совместимости. Щелкните по ссылкам на исходный код и поищите их. Я многому научился, потратив время на изучение местности. - person Andrew Philips; 26.07.2021

Это невозможно из коробки, поскольку валидатор запускается напрямую, когда событие input используется для запуска обновлений. См. Эту строку в исходном коде:

Если вы хотите использовать время устранения дребезга на этом уровне, вам нужно получить наблюдаемое, напрямую связанное с событием input соответствующего элемента DOM. Эта проблема в Github может дать вам контекст:

В вашем случае обходным путем было бы реализовать специальный метод доступа к значениям, использующий fromEvent метод наблюдаемого.

Вот образец:

const DEBOUNCE_INPUT_VALUE_ACCESSOR = new Provider(
  NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => DebounceInputControlValueAccessor), multi: true});

@Directive({
  selector: '[debounceTime]',
  //host: {'(change)': 'doOnChange($event.target)', '(blur)': 'onTouched()'},
  providers: [DEBOUNCE_INPUT_VALUE_ACCESSOR]
})
export class DebounceInputControlValueAccessor implements ControlValueAccessor {
  onChange = (_) => {};
  onTouched = () => {};
  @Input()
  debounceTime:number;

  constructor(private _elementRef: ElementRef, private _renderer:Renderer) {

  }

  ngAfterViewInit() {
    Observable.fromEvent(this._elementRef.nativeElement, 'keyup')
      .debounceTime(this.debounceTime)
      .subscribe((event) => {
        this.onChange(event.target.value);
      });
  }

  writeValue(value: any): void {
    var normalizedValue = isBlank(value) ? '' : value;
    this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
  }

  registerOnChange(fn: () => any): void { this.onChange = fn; }
  registerOnTouched(fn: () => any): void { this.onTouched = fn; }
}

И используйте это так:

function validator(ctrl) {
  console.log('validator called');
  console.log(ctrl);
}

@Component({
  selector: 'app'
  template: `
    <form>
      <div>
        <input [debounceTime]="2000" [ngFormControl]="ctrl"/>
      </div>
      value : {{ctrl.value}}
    </form>
  `,
  directives: [ DebounceInputControlValueAccessor ]
})
export class App {
  constructor(private fb:FormBuilder) {
    this.ctrl = new Control('', validator);
  }
}

См. Этот плагин: https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview .

person Thierry Templier    schedule 29.04.2016
comment
асинхронный валидатор работает отлично, но мои другие валидаторы, похоже, не работают, например. * ngIf = (email.touched && email.errors) не запускается - person Chanlito; 30.04.2016

альтернативным решением с RxJs может быть следующее.

/**
 * From a given remove validation fn, it returns the AsyncValidatorFn
 * @param remoteValidation: The remote validation fn that returns an observable of <ValidationErrors | null>
 * @param debounceMs: The debounce time
 */
debouncedAsyncValidator<TValue>(
  remoteValidation: (v: TValue) => Observable<ValidationErrors | null>,
  remoteError: ValidationErrors = { remote: "Unhandled error occurred." },
  debounceMs = 300
): AsyncValidatorFn {
  const values = new BehaviorSubject<TValue>(null);
  const validity$ = values.pipe(
    debounceTime(debounceMs),
    switchMap(remoteValidation),
    catchError(() => of(remoteError)),
    take(1)
  );

  return (control: AbstractControl) => {
    if (!control.value) return of(null);
    values.next(control.value);
    return validity$;
  };
}

Использование:

const validator = debouncedAsyncValidator<string>(v => {
  return this.myService.validateMyString(v).pipe(
    map(r => {
      return r.isValid ? { foo: "String not valid" } : null;
    })
  );
});
const control = new FormControl('', null, validator);
person Alberto Aldegheri    schedule 12.04.2018

Вот служба, которая возвращает функцию валидатора, которая использует debounceTime(...) и distinctUntilChanged():

@Injectable({
  providedIn: 'root'
})
export class EmailAddressAvailabilityValidatorService {

  constructor(private signupService: SignupService) {}

  debouncedSubject = new Subject<string>();
  validatorSubject = new Subject();

  createValidator() {

    this.debouncedSubject
      .pipe(debounceTime(500), distinctUntilChanged())
      .subscribe(model => {

        this.signupService.checkEmailAddress(model).then(res => {
          if (res.value) {
            this.validatorSubject.next(null)
          } else {
            this.validatorSubject.next({emailTaken: true})
          }
        });
      });

    return (control: AbstractControl) => {

      this.debouncedSubject.next(control.value);

      let prom = new Promise<any>((resolve, reject) => {
        this.validatorSubject.subscribe(
          (result) => resolve(result)
        );
      });

      return prom
    };
  }
}

Использование:

emailAddress = new FormControl('',
    [Validators.required, Validators.email],
    this.validator.createValidator() // async
);

Если вы добавите валидаторы Validators.required и Validators.email, запрос будет выполнен только в том случае, если входная строка не пуста и является допустимым адресом электронной почты. Это нужно делать, чтобы избежать ненужных вызовов API.

person Willi Mentzel    schedule 17.11.2018
comment
Если distinctUntilChanged() не удается, я думаю, что signupService не будет выполняться, поэтому ничего не будет передано в validatorSubject, и форма застрянет в состоянии PENDING. - person funkid; 01.06.2019

Вот пример из моего живого проекта Angular с использованием rxjs6

import { ClientApiService } from '../api/api.service';
import { AbstractControl } from '@angular/forms';
import { HttpParams } from '@angular/common/http';
import { map, switchMap } from 'rxjs/operators';
import { of, timer } from 'rxjs/index';

export class ValidateAPI {
  static createValidator(service: ClientApiService, endpoint: string, paramName) {
    return (control: AbstractControl) => {
      if (control.pristine) {
        return of(null);
      }
      const params = new HttpParams({fromString: `${paramName}=${control.value}`});
      return timer(1000).pipe(
        switchMap( () => service.get(endpoint, {params}).pipe(
            map(isExists => isExists ? {valueExists: true} : null)
          )
        )
      );
    };
  }
}

и вот как я использую его в своей реактивной форме

this.form = this.formBuilder.group({
page_url: this.formBuilder.control('', [Validators.required], [ValidateAPI.createValidator(this.apiService, 'meta/check/pageurl', 'pageurl')])
});
person Дніщенко Денис    schedule 14.08.2018

Пример RxJS 6:

import { of, timer } from 'rxjs';
import { catchError, mapTo, switchMap } from 'rxjs/operators';      

validateSomething(control: AbstractControl) {
    return timer(SOME_DEBOUNCE_TIME).pipe(
      switchMap(() => this.someService.check(control.value).pipe(
          // Successful response, set validator to null
          mapTo(null),
          // Set error object on error response
          catchError(() => of({ somethingWring: true }))
        )
      )
    );
  }
person Dima    schedule 07.08.2018
comment
Следует отметить, что в более поздних версиях RxJS timer () больше не является статической функцией в Observable. - person AlanObject; 06.09.2019
comment
как получилось @AlanObject? rxjs-dev.firebaseapp.com/api/index/function/timer - person Dima; 09.06.2020
comment
Честно говоря, я не могу вспомнить, в чем была проблема. - person AlanObject; 10.06.2020

Все можно немного упростить

export class SomeAsyncValidator {
   static createValidator = (someService: SomeService) => (control: AbstractControl) =>
       timer(500)
           .pipe(
               map(() => control.value),
               switchMap((name) => someService.exists({ name })),
               map(() => ({ nameTaken: true })),
               catchError(() => of(null)));
}
person V.Ottens    schedule 27.04.2020

Всем, кто все еще интересуется этой темой, важно отметить это в документе angular 6:

  1. Они должны вернуть обещание или наблюдаемое,
  2. Возвращаемое наблюдаемое должно быть конечным, что означает, что оно должно завершиться в какой-то момент. Чтобы преобразовать бесконечное наблюдаемое в конечное, пропустите наблюдаемое через оператор фильтрации, например first, last, take или takeUntil.

Будьте осторожны со вторым требованием выше.

Вот реализация AsyncValidatorFn:

const passwordReapeatValidator: AsyncValidatorFn = (control: FormGroup) => {
  return of(1).pipe(
    delay(1000),
    map(() => {
      const password = control.get('password');
      const passwordRepeat = control.get('passwordRepeat');
      return password &&
        passwordRepeat &&
        password.value === passwordRepeat.value
        ? null
        : { passwordRepeat: true };
    })
  );
};
person Marvin    schedule 06.09.2018

Попробуйте с таймером.

static verificarUsuario(usuarioService: UsuarioService) {
    return (control: AbstractControl) => {
        return timer(1000).pipe(
            switchMap(()=>
                usuarioService.buscar(control.value).pipe(
                    map( (res: Usuario) => { 
                        console.log(res);
                        return Object.keys(res).length === 0? null : { mensaje: `El usuario ${control.value} ya existe` };
                    })
                )
            )
        )
    }
}
person ale7    schedule 29.12.2020

Поскольку мы пытаемся уменьшить количество запросов, которые мы отправляем на сервер, я бы также рекомендовал добавить проверку, чтобы на сервер отправлялись только действительные электронные письма для проверки.

Я использовал простой RegEx из JavaScript: HTML Form - проверка адреса электронной почты

Мы также используем timer(1000) для создания Observable, который выполняется через 1 с.

Установив эти два элемента, мы проверяем адрес электронной почты только в том случае, если он действителен, и только через 1 секунду после ввода пользователя. switchMap оператор отменит предыдущий запрос, если сделан новый запрос


const emailRegExp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
const emailExists = control =>
  timer(1000).pipe(
    switchMap(() => {
      if (emailRegExp.test(control.value)) {
        return MyService.checkEmailExists(control.value);
      }
      return of(false);
    }),
    map(exists => (exists ? { emailExists: true } : null))
  );

Затем мы можем использовать этот валидатор с функцией Validator.pattern()

  myForm = this.fb.group({
    email: [ "", { validators: [Validators.pattern(emailRegExp)], asyncValidators: [emailExists] }]
  });

Ниже приведен образец демонстрации на stackblitz

person Owen Kelvin    schedule 15.04.2021
comment
Регулярное выражение, которое вы используете, слишком простое; он исключает + псевдонимы и общие TLD, которые являются нормальными и действительными частями электронного письма, поэтому [email protected] не будет работать. В Angular уже есть валидатор электронной почты, доступный в Validators.email, который вы можете указать в списке валидаторов элемента управления и для проверки в асинхронном валидаторе. - person doppelgreener; 15.04.2021
comment
@doppelgreener спасибо за информацию, я обновил решение с улучшенным RegExp - person Owen Kelvin; 15.04.2021

У меня такая же проблема. Мне нужно было решение для отмены ввода и запрашивать серверную часть только при изменении ввода.

У всех обходных путей с таймером в валидаторе есть проблема, заключающаяся в том, что они запрашивают серверную часть при каждом нажатии клавиши. Они только отклоняют ответ проверки. Это не то, что намеревались делать. Вы хотите, чтобы ввод был обработан и выделен, и только после этого запрашивать бэкэнд.

Мое решение для этого следующее (с использованием реактивных форм и материала2):

Компонент

@Component({
    selector: 'prefix-username',
    templateUrl: './username.component.html',
    styleUrls: ['./username.component.css']
})
export class UsernameComponent implements OnInit, OnDestroy {

    usernameControl: FormControl;

    destroyed$ = new Subject<void>(); // observes if component is destroyed

    validated$: Subject<boolean>; // observes if validation responses
    changed$: Subject<string>; // observes changes on username

    constructor(
        private fb: FormBuilder,
        private service: UsernameService,
    ) {
        this.createForm();
    }

    ngOnInit() {
        this.changed$ = new Subject<string>();
        this.changed$

            // only take until component destroyed
            .takeUntil(this.destroyed$)

            // at this point the input gets debounced
            .debounceTime(300)

            // only request the backend if changed
            .distinctUntilChanged()

            .subscribe(username => {
                this.service.isUsernameReserved(username)
                    .subscribe(reserved => this.validated$.next(reserved));
            });

        this.validated$ = new Subject<boolean>();
        this.validated$.takeUntil(this.destroyed$); // only take until component not destroyed
    }

    ngOnDestroy(): void {
        this.destroyed$.next(); // complete all listening observers
    }

    createForm(): void {
        this.usernameControl = this.fb.control(
            '',
            [
                Validators.required,
            ],
            [
                this.usernameValodator()
            ]);
    }

    usernameValodator(): AsyncValidatorFn {
        return (c: AbstractControl) => {

            const obs = this.validated$
                // get a new observable
                .asObservable()
                // only take until component destroyed
                .takeUntil(this.destroyed$)
                // only take one item
                .take(1)
                // map the error
                .map(reserved => reserved ? {reserved: true} : null);

            // fire the changed value of control
            this.changed$.next(c.value);

            return obs;
        }
    }
}

Шаблон

<mat-form-field>
    <input
        type="text"
        placeholder="Username"
        matInput
        formControlName="username"
        required/>
    <mat-hint align="end">Your username</mat-hint>
</mat-form-field>
<ng-template ngProjectAs="mat-error" bind-ngIf="usernameControl.invalid && (usernameControl.dirty || usernameControl.touched) && usernameControl.errors.reserved">
    <mat-error>Sorry, you can't use this username</mat-error>
</ng-template>
person rkd    schedule 14.04.2018
comment
это именно то, что я ищу, но где именно вы здесь выполняете http-вызовы? Моя основная проблема в том, что каждое нажатие клавиши запускает вызов backend-api - person Mustafa; 13.06.2018
comment
this.service.isUsernameReserved(username).subscribe(reserved => this.validated$.next(reserved)); http-вызов находится внутри службы. - person rkd; 25.06.2018