Why distinctUntilChanged won't work in asyncValidators in angular

1.3k views Asked by At

When I press a key on a form input for some reason the async validator doesn't detect the distinctUntilChanged and it still sends API requests. for example if I press 35, delete 5 and after that add 5 again it still sends the request. this is the code: (I've tried pretty much everything but still doesn't work)

validateSomeNumber(control: FormControl): Observable<any> | Promise <any> {
    this.isSubmitBtnDisabled = true;
    return control.valueChanges.pipe(
      debounceTime(1000),
      distinctUntilChanged(),
      switchMap((value) => {
        return this.apiService.someApiRequest({ 'to_number': control.value }).pipe(
          map(res => {
            console.log(res);
            if (res.success) {
              // console.log(res);
              this.isSubmitBtnDisabled = false;
              return null;
            } else {
              // console.log(res);
              this.isSubmitBtnDisabled = true;
              return{ 'invalidCharacters': true };
            }
          }),
        );
      }),
      first()
    );
  }
1

There are 1 answers

3
frido On

By default validateSomeNumber is called after every value change.

If you return this on every value change

return control.valueChanges.pipe(
  debounceTime(1000),
  distinctUntilChanged(),
  ...
)

you're creating a new Observable of value changes on every value change. e.g if you type four characters you end up with four independent Observables each emitting one character and not with one Observable emitting four times. So debounceTime and distinctUntilChanged will only effecting the Observable you create on a particular value change but not the value change process as a whole. If they only effect an Observable that emits once they obviously don't work as you intend them to do.

You should return the http request directly

validateSomeNumber(control: FormControl): Observable<any> | Promise <any> {
  this.isSubmitBtnDisabled = true;
  return this.apiService.someApiRequest({ 'to_number': control.value }).pipe(
      map(..),
  );
}

Limiting the request frequency

Option 1: updateOn

To prevent the http request from being executed on every value change Angular recommends changing the updateOn property to submit or blur.

With template-driven forms:

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

With reactive forms:

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

{updateOn: 'blur'} will only execute the validators when your input looses focus.

Option 2: Emulate debounceTime and distinctUntilChanged

Angular automatically unsubscribes from the previous Observable returned by the AsyncValidator if the form value changes. This allows you to emulate debounceTime with timer. To emulate distinctUntilChanged you can keep track of the last request term and do the equality check yourself.

private lastRequestTerm = null;

validateSomeNumber(control: FormControl): Observable<any> | Promise <any> {
  this.isSubmitBtnDisabled = true;
  // emulate debounceTime
  return timer(1000).pipe(
    // emulate distinceUntilChanged
    filter(_ => control.value != this.lastRequestTerm),
    switchMap(() => {
      this.lastSearchTerm = control.value;
      return this.apiService.someApiRequest({ 'to_number': control.value });
    }),
    map(..)
  );
}