简体   繁体   中英

Debounce async Validator

I have a working async validator that does an HTTP request to the server to check if a username is already taken. As I don't want to call the API after each keystroke I need to debounce the input stream.

I first had throttleTime in the service, but another topic on SO said this has to be on the one who subscribes, but no luck yet!

My Component:

this.form = this._fb.group(
      {
        username: ['', [Validators.required, Validators.maxLength(50), NoWhitespaceValidator], [IsUserIdFreeValidator.createValidator(this._managementService)]]
      });

My Validator:

export class IsUserIdFreeValidator {
  static createValidator(_managementService: ManagementService) {
    return (control: AbstractControl) => {
      return _managementService.isUserIdFree(control.value)
        .pipe(
          throttleTime(5000),
          (map(
            (result: boolean) => result === false ? { isUserIdFree: true } : null))
        );
    };
  }
}

My Service:

  public isUserIdFree(userId: string): Observable<{} | boolean | HttpError> {
    const updateUserCheck: UpdateUserCheck = new UpdateUserCheck();
    updateUserCheck.userID = userId;

    return this._httpClient.post<boolean>('UserManagementUser/IsUserIdFree', updateUserCheck));
  }

This should do the trick:

  static createValidator(_managementService: ManagementService) {
    const subject = new BehaviorSubject('');
    const observable = subject.asObservable().pipe(
        debounceTime(1000),
        switchMap(val => _managementService.isUserIdFree(val)),
        map((isUserIdFree: boolean) => isUserIdFree ?  null : { userIdTaken : true }),
        ); 
    return (control: AbstractControl) => {
      subject.next(control.value);
      return observable.pipe(takeUntil(timer(5000))); // acts as a way to make observable finite
    }
  }

The debouncing should happen on the value emitted by the control as opposed to the result returned from the http service. We start by emitting the value on an observable stream and piping it through distinctUntilChanged , which ensures that only a distinct value compared to the last emitted value gets past that stage of the pipe line. debounceTime(x) ensures only the last value after an 'x' amount of milliseconds emits.

The switchMap operator takes the control value and fires a get request to the backend and passes the new observable to the next stage of the pipeline. Finally, I have applied your existing map operator on the result from the backend to generate an appropriate error message.

I was able to solve this trough another way, via a propery called ' updateOn ' on the control.

With template-driven forms:

<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">

With reactive forms:

new FormControl('', {updateOn: 'blur'});

If you use the form-builder, since Angular 7 you can use this :

this.form = this._fb.group({
  email: ['', [CustomValidators.email], null, 'blur'],
});

Using observables, I did:

form = this._fb.group({
email: [null, [Validators.required, Validators.email], [this.validateEmailNotTaken.bind(this)]]
})

private _emailCancel$ = new Subject<void>();
 validateEmailNotTaken(control: AbstractControl): Observable<object> {
        this._emailCancel$.next();
        return race(
            timer(3000).pipe(map(_ => true)),
            this._emailCancel$.pipe(
                map(_ => false)
            )
        ).pipe(
            take(1),
            filter(val => !!val),
            switchMap(() => this.isEmailTaken(control.value).pipe(
                map(res => res ? {isTaken: true } : null)
            ))
        );
    }

To anyone finding this question and set of answers, IMHO, this question is a duplicate of this one . I think this answer is the correct one, unless you like Promises. I intend to post an answer there with a newer take (Angular 9+) solution.

EDIT: here's my suggestion for how to do this.

Incidentally, before coming across this question and the other one I tried all three of the answers listed here. You can get them all to work, but the answer I linked to is far and away the most elegant. It's hard to figure out because it relies on behavior buried in the Angular framework that really ought to be in the docs.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM