106

This is my Async Validator it doesn't have a debounce time, how can I add it?

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});
          })
      })
    }
}
2
  • I think that it's not possible... I asked the question in the past but have no answers: github.com/angular/angular/issues/6895. Commented Apr 28, 2016 at 15:25
  • @ThierryTemplier so do you have a way around that problem? Commented Apr 28, 2016 at 15:30

16 Answers 16

130

Angular 4+, Using Observable.timer(debounceTime) :

@izupet 's answer is right but it is worth noticing that it is even simpler when you use Observable:

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

Since angular 4 has been released, if a new value is sent for checking, Angular unsubscribes from Observable while it's still paused in the timer, so you don't actually need to manage the setTimeout/clearTimeout logic by yourself.

Using timer and Angular's async validator behavior we have recreated RxJS debounceTime.

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

10 Comments

IMHO this is by far the most elegant solution for the "debounce" problem. Note: there is no subscribe() because when returning an Observable instead of a Promise the Observable must be cold.
problem solved, I was sending async validator alongside other validators.
@SamanMohamadi yeah was doing the same thing. To complete your comment, Angular has a third param that need to be passed for async validation: this.formBuilder.group({ fieldName: [initialValue, [SyncValidators], [AsyncValidators]] });
@ChristianCederquist yes. also note that with Angular 6, Observable.timer have been changed for simply timer, and switchMap must be used with the pipe operator, so it give : timer(500).pipe(switchMap(()=>{}))
In face, the http request is canceled because the formControl unsubscribes from the observable. it is not because of switchMap. you could use mergeMap or ConcatMap with the same effect, because Timer emit only once.
|
94
+100

Keep it simple: no timeout, no delay, no custom Observable

// 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
}

5 Comments

Possibly the best solution here
Using code similar to this but for me the debounce/distinctUntilChanged doesn't seem to do anything - validator fires immediately after each keypress.
Looks good but still doesn't seem to work for Angular Async validators
This will not work. The validator is waiting for a valueChanges event on the control that is running it's validator because a valueChanges event ran. The next change will unsubscribe the previous validator before running the next validation. This may appear to work but will fail on a slow enough process and always required another change to validate the last.
This has issues - the validator instantiates in a pending state marking the whole form as PENDING (which weirdly allows submission). Also - as pointed out - distinctUntilChanged() does not work. I don't think it can ever work in an AsyncValidatorFn. This is because a brand new Observable is created for each keystroke. The framework handles this (by replacement) but anything downstream is thrown away. I played around with a singleton Observable outside the scope of AsyncValidatorFn - this also suffers from the PENDING issue.
72

Angular 9+ asyncValidator w/ debounce

@n00dl3 has the correct answer. I love relying on the Angular code to unsubscribe and create a new async validator by throwing in a timed pause. Angular and RxJS APIs have evolved since that answer was written, so I'm posting some updated code.

Also, I made some changes. (1) The code should report a caught error, not hide it under a match on the email address, otherwise we will confuse the user. If the network's down, why say the email matched?! UI presentation code will differentiate between email collision and network error. (2) The validator should capture the control's value prior to the time delay to prevent any possible race conditions. (3) Use delay instead of timer because the latter will fire every half second and if we have a slow network and email check takes a long time (one second), timer will keep refiring the switchMap and the call will never complete.

Angular 9+ compatible fragment:

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: Anyone wanting to dig deeper into the Angular sources (I highly recommend it), you can find the Angular code that runs asynchronous validation here and the code that cancels subscriptions here which calls into this. All the same file and all under updateValueAndValidity.

6 Comments

I really like this answer. timer was working for me until it wasn't. It was successfully cancelling the api request when the next validation would fire but it should not of made an api request in the first place. This solution is working well so far.
of(control.value) seems arbitrary at first (as it could be of(anything)), but it gives a bonus of being able to change the name of control.value to email.
It does feel a bit arbitrary and in reviewing the Angular code there's no apparent reason for this value to change before the switchMap call; the entire point of this exercise is to only use a value that's 'settled' and a changed value would trigger re-asyncValidation. However, the defensive programmer in me says lock in the value at creation time because code lives forever and underlying assumptions can always change.
Implemented this and it works great. Thanks! This should be the accepted answer imo.
So just to be clear, the reason this works is that Angular will cancel pending async validators before starting a new run of them when the value changes, right? This is a lot simpler than trying to debounce the control value like several other answers wanted to do.
|
32

It is actually pretty simple to achieve this (it is not for your case but it is general example)

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);
    });
}

3 Comments

I think this is the better solution. Because @Thierry Templier's solution wil delay all validation rules, not just the async one.
@n00dl3's solution is more elegant and since rxjs is already available, why not use it to simplify matters more
@BobanStojanovski that question refers to angular 2. My solution works only with angular 4+.
11

It's not possible out of the box since the validator is directly triggered when the input event is used to trigger updates. See this line in the source code:

If you want to leverage a debounce time at this level, you need to get an observable directly linked with the input event of the corresponding DOM element. This issue in Github could give you the context:

In your case, a workaround would be to implement a custom value accessor leveraging the fromEvent method of observable.

Here is a sample:

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; }
}

And use it this way:

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);
  }
}

See this plunkr: https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview.

1 Comment

the async validator works great but my others validator doesn't seem to work though, e.g. *ngIf="(email.touched && email.errors) doesn't get triggered
5

an alternative solution with RxJs can be the following.

/**
 * 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$;
  };
}

Usage:

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);

Comments

5

Here is an example from my live Angular project using 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)
          )
        )
      );
    };
  }
}

and here is how I use it in my reactive form

this.form = this.formBuilder.group({
page_url: this.formBuilder.control('', [Validators.required], [ValidateAPI.createValidator(this.apiService, 'meta/check/pageurl', 'pageurl')])
});

1 Comment

This is actually a great answer. Having the timer be first ensures that if more validations are triggered, Angular Forms will automatically cancel them, before the API call is ever made. So there is no concern that you're creating endless timers. You will only have a single timer pending at any given time.
5

Here a service that returns a validator function that uses debounceTime(...) and 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
    };
  }
}

Usage:

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

If you add the validators Validators.required and Validators.email the request will only be made if the input string is non-empty and a valid email address. This should be done to avoid unnecessary API calls.

1 Comment

If distinctUntilChanged() fails, I think the signupService won't execute therefore nothing will emit to the validatorSubject, and the form will stuck in PENDING status.
2

RxJS 6 example:

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 }))
        )
      )
    );
  }

3 Comments

It should be noted that in later versions of RxJS timer() is no longer a static function in Observable.
I honestly can't remember what the issue was.
2

Things can be simplified a little bit

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)));
}

Comments

1

Since we are trying to reduce the number of request we are making to the server, I would also recommend adding a check to ensure only valid emails are sent to the server for checking

I have used a simple RegEx from JavaScript: HTML Form - email validation

We are also using timer(1000) to create an Observable that executes after 1s.

With this two items set up, we only check an email address if it is valid and only after 1s after user input. switchMap operator will cancel previous request if a new request is made


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))
  );

We can then use this validator with the Validator.pattern() function

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

Below is a Sample demo on stackblitz

2 Comments

The regex you're using is a bit too simple; it shuts out + aliasing and generic TLDs, both of which are normal and valid parts of an email, so [email protected] would not work. Angular already has an email validator available in Validators.email, which you could provide in the control's validators list and test against in the async validator.
@doppelgreener thanks for the info, I have updated the solution with a better RegExp
0

To anyone still interested in this subject, it's important to notice this in angular 6 document:

  1. They must return a Promise or an Observable,
  2. The observable returned must be finite, meaning it must complete at some point. To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as first, last, take, or takeUntil.

Be careful with the 2nd requirement above.

Here's a AsyncValidatorFn implementation:

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 };
    })
  );
};

Comments

0

Try with timer.

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` };
                    })
                )
            )
        )
    }
}

Comments

0

what @Pavel says is a good solution, but if the form has a previous value, it should be something like this...

private checkEmailAvailabilityValidator(): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors> =>
    control.value
      ? of(control.value).pipe(
          delay(400),
          distinctUntilChanged(),
          switchMap(() => this.professionalWorkersService.checkEmailAvailability(control.value, this.workerId)),
          map(unique => (unique ? {} : { unavailableEmail: true }))
        )
      : of();
}

Comments

0

Async validator directive with cancel, with comparing with prev value, and debounce

export const IS_VALUE_EXISTS = 'IS_VALUE_EXISTS';
export const DEBOUNCE_TIME = 1200;

@Directive({
    selector: '[isValueExistsValidator]',
    providers: [
        {
            provide: NG_ASYNC_VALIDATORS,
            useExisting: IsValueExistsValidatorDirective ,
            multi: true,
        },
    ],
})
export class IsValueExistsValidatorDirective implements AsyncValidator, OnDestroy {

    private destroyed$: Subject<void> = new Subject<void>();

    private cancel$: Subject<void> = new Subject<void>();

    private lastInput = null;

    private lastOutput$: Observable<ValidationErrors | null> = new BehaviorSubject(null);

    constructor(private existanceService: ExistanceService) {}

    validate({ value }: AbstractControl): Observable<ValidationErrors | null> {
        const trimmedValue = value ? value.trim() : value;
        if (this.lastInput === trimmedValue) {
            return this.lastOutput$;
        }

        this.lastInput = trimmedValue;

        this.cancel$.next();
        this.cancel$.complete();
        this.cancel$ = new Subject<void>();

        this.lastOutput$ = timer(DEBOUNCE_TIME).pipe(
            switchMap(() => this.existanceService.isExists(this.lastInput)),
            map((result) => (result ? { [IS_VALUE_EXISTS ]: result } : null)),
            takeUntil(this.destroyed$.pipe(mergeWith(this.cancel$))),
            shareReplay(1),
        );

        return this.lastOutput$;
    }

    public ngOnDestroy(): void {
        this.destroyed$.next();
        this.destroyed$.complete();
    }
}

Example of usage:

 <input [isValueExistsValidator] formControlName="controlName"/>

Comments

-2

I had the same problem. I wanted a solution for debouncing the input and only request the backend when the input changed.

All workarounds with a timer in the validator have the problem, that they request the backend with every keystroke. They only debounce the validation response. That's not what's intended to do. You want the input to be debounced and distincted and only after that to request the backend.

My solution for that is the following (using reactive forms and material2):

The component

@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;
        }
    }
}

The template

<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>

2 Comments

this is exactly what im looking for, but where exactly do you do the http calls here? my main issue is every keypress is firing a backend-api call
this.service.isUsernameReserved(username).subscribe(reserved => this.validated$.next(reserved)); the http call is within the service.

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.