4

I want to add debounceTime and distinctUntilChanged in my async validator.

mockAsyncValidator(): AsyncValidatorFn {
    return (control: FormControl): Observable<ValidationErrors | null> => {
      return control.valueChanges.pipe(
        debounceTime(500),
        distinctUntilChanged(),
        switchMap(value => {
          console.log(value);  // log works here
          return this.mockService.checkValue(value).pipe(response => {
            console.log(response);  // log did not work here
            if (response) {
              return { invalid: true };
            }
            return null;
          })
        })
      );
  }

The code above did not work, the form status becomes PENDING.
But when I use timer in this answer, the code works, but I can't use distinctUntilChanged then.

return timer(500).pipe(
    switchMap(() => {
      return this.mockService.checkValue(control.value).pipe(response => {
        console.log(response);  // log works here
        if (response) {
          return { invalid: true };
        }
        return null;
      })
    })
  );

I tried to use BehaviorSubject like

debouncedSubject = new BehaviorSubject<string>('');

and use it in the AsyncValidatorFn, but still not work, like this:

this.debouncedSubject.next(control.value);
return this.debouncedSubject.pipe(
  debounceTime(500),
  distinctUntilChanged(), // did not work
                          // I think maybe it's because of I next() the value
                          // immediately above
                          // but I don't know how to fix this
  take(1), // have to add this, otherwise, the form is PENDING forever
           // and this take(1) cannot add before debounceTime()
           // otherwise debounceTime() won't work
  switchMap(value => {
    console.log(value); // log works here
    return this.mockService.checkValue(control.value).pipe(response => {
        console.log(response);  // log works here
        if (response) {
          return { invalid: true };
        }
        return null;
      }
    );
  })
);
Udara Abeythilake
  • 1,215
  • 1
  • 20
  • 31
funkid
  • 577
  • 1
  • 10
  • 30
  • It turns out that `distinctUntilChanged()` seems not really apply to form async validations. References [here](https://stackoverflow.com/q/51380618/10684507) and [here](https://stackoverflow.com/q/46889851/10684507). – funkid Jun 01 '19 at 06:36

1 Answers1

1

The problem is that a new pipe is being built every time the validatorFn executes as you are calling pipe() inside the validatorFn. The previous value isn't capture for discinct or debounce to work. What you can do is setup two BehaviourSubjects externally, termDebouncer and validationEmitter in my case.

You can set up a factory method to create this validator and thereby re-use it. You could also extend AsyncValidator and create a class with DI setup. I'll show the factory method below.

export function AsyncValidatorFactory(mockService: MockService) { 
  const termDebouncer = new BehaviorSubject('');
  const validationEmitter = new BehaviorSubject<T>(null);
  let prevTerm = '';
  let prevValidity = null;

  termDebouncer.pipe(
        map(val => (val + '').trim()),
        filter(val => val.length > 0),
        debounceTime(500),
        mergeMap(term => { const obs = term === prevTerm ? of(prevValidity) : mockService.checkValue(term);
          prevTerm = term; 
          return obs; }),
        map(respose => { invalid: true } : null),
        tap(validity => prevValidity = validity)
    ).subscribe(validity => validationEmitter.next(validity))


  return (control: AbstractControl) => {
    termDebouncer.next(control.value)
    return validationEmitter.asObservable().pipe(take(2))
  }
}

Edit: This code excerpt is from a use case other than Angular form validation, (React search widget to be precise.) the pipe operators might need changing to fit your use case.

Edit2: take(1) or first() to ensure that the observable completes after emitting the validation message. asObservable() will ensure that a new observable will be generated on the next call. You might also be able to skip asObservable() and just pipe() as the pipe operator branches the async pipeline and creates a new observable from there onwards. You might have to use take(2) to get past the fact that a behaviourSubject is stateful and holds a value.

Edit3: Use a merge map to deal with the fact distinctUntilChanged() will cause the observable to not emit and not complete.

Avin Kavish
  • 8,317
  • 1
  • 21
  • 36
  • I think `debounceTime()` should be called before `distinctUntilChanged()`. And since the `val` in `termDebouncer`'s type is `string` trivially, it seems that you can use `val.trim()` directly in the first `map()`. – funkid May 31 '19 at 15:29
  • This is an excerpt from one of my projects where the value could be a `number` or `string`. I've copied and pasted some and added some. Yep you are right on the `distinctUntilChanged()` – Avin Kavish May 31 '19 at 15:45
  • I found that although `debounceTime()` and `distinctUntilChanged()` work fine now, it will make the form stuck in `PENDING` status. `take(1)` or `first()` not seems to be the right way to fix this since either of them will "complete" the observer and never accept the value emit from the control after. – funkid Jun 01 '19 at 03:32
  • no it should be okay to use them because the statement, `validationEmitter.asObservable()` is called every time the input is validated and you can pipe it there. – Avin Kavish Jun 01 '19 at 04:16
  • You're right on this, I wrongly put the `take()` pipe onto `termDebouncer`. Now the async validation can complete. But here comes another dilemma situation: if `distinctUntilChanged()` fails, the `mockService` won't execute so nothing will emit to the `validationEmitter`, and form stucks in `PENDING` status again. – funkid Jun 01 '19 at 05:17
  • I've made a change. Look, I've shown you how to capture and reuse behaviour subjects which is what was holding you back originally. I can't keep helping you account for every possible scenario. That's your job. For example: this will not work if there's a network error. This link explains all the operators you need to make observables work, the rest is up to you. https://www.learnrxjs.io/operators – Avin Kavish Jun 01 '19 at 05:58