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;
      }
    );
  })
);
1
  • It turns out that distinctUntilChanged() seems not really apply to form async validations. References here and here. Commented Jun 1, 2019 at 6:36

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

6 Comments

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().
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()
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.
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.
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.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.