98

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});
          })
      })
    }
}
doppelgreener
  • 4,809
  • 10
  • 46
  • 63
Chanlito
  • 2,322
  • 2
  • 17
  • 27

15 Answers15

123

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.

Emil
  • 7,220
  • 17
  • 76
  • 135
n00dl3
  • 21,213
  • 7
  • 66
  • 76
  • 10
    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_. – Bernhard Fürst Aug 24 '17 at 14:19
  • doesn't work for me, observable without subscribe doesn't fire at all. – Saman Mohamadi Dec 15 '17 at 09:44
  • 1
    problem solved, I was sending async validator alongside other validators. – Saman Mohamadi Dec 15 '17 at 10:29
  • 1
    @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]] });` – guilima Apr 13 '18 at 12:25
  • Does switchMap cancel the HTTP request when the input changes and the FormControl unsubscribes from the observable? – Christian Cederquist Jun 20 '18 at 06:54
  • 30
    @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(()=>{}))` – Félix Brunet Jun 27 '18 at 19:37
  • 1
    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. – Félix Brunet Jun 27 '18 at 19:53
  • 1
    I just wanted to clarify the relationship between RxJS cancelling, and Angular's Validation behaviour unsubscribing when input changes :-) If I understand correctly, the switchMap documentation talks about a separate cancelling behaviour when there's new emissions from the source observable, while the cancelling that happens here actually comes from the unsubscribe() call from Angular. – Christian Cederquist Jun 29 '18 at 09:00
  • 1
    most elegant solution – Rahul Jujarey Jan 11 '19 at 22:59
  • private validateUniqueValueEmail(control): Observable { return timer(500).pipe( switchMap(() => { return of({validationError: 'This email is already taken'}); }), ); } after wring function like this i get this error ERROR TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable. – Liu Zhang Mar 26 '19 at 10:53
  • Love this answer and am going to implement it. One minor item, I think `control.value` should be sent in at the beginning of the Observable, rather than fetched at `checkEmail` because things could have changed while delayed in `timer` (control.value, form structure). Yes, I know it'll likely be cancelled when this happens, and this is this function's entire purpose, but code (especially async code) should maintain a proper input surface in case of race conditions. Proposed modification would be: `of(control.value).pipe(timer(500),switchMap((email)=>...)`. – Andrew Philips Jun 29 '20 at 14:50
78

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
}
doppelgreener
  • 4,809
  • 10
  • 46
  • 63
Pavel
  • 2,557
  • 1
  • 23
  • 19
  • 13
    Possibly the best solution here –  Jul 16 '19 at 16:38
  • 6
    Using code similar to this but for me the debounce/distinctUntilChanged doesn't seem to do anything - validator fires immediately after each keypress. – Rick Strahl Nov 19 '19 at 20:58
  • 4
    Looks good but still doesn't seem to work for Angular Async validators – Laszlo Sarvold Jun 29 '20 at 10:52
  • 9
    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. – Jacob Roberts Sep 25 '20 at 13:38
57

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.

Jonathan
  • 3,893
  • 5
  • 46
  • 77
Andrew Philips
  • 1,950
  • 18
  • 23
  • 1
    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. – Jacob Roberts Sep 25 '20 at 13:41
  • 1
    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. – ScubaSteve May 18 '21 at 17:56
  • 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. – Andrew Philips May 18 '21 at 20:10
  • 1
    Implemented this and it works great. Thanks! This should be the accepted answer imo. – Geo242 Jul 08 '21 at 21:42
  • 1
    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. – Coderer Jul 26 '21 at 15:14
  • Yes, you are correct. It took me a little while to wrap my head around @n00dl3's idea because unlike the RxJS `debounce` operator which runs inline for an Observable, this emulates that behavior through a couple of code paths because the asyncValidator isn't built like an Observable using switchMap. Without knowing the history of this code, I'm guessing the async validation API predates the cleaner Observable switchMap and once built, dev team left it as-is for backward compatibility. Click on those source code links and poke around. I learned a lot spending time trying to understand the area. – Andrew Philips Jul 26 '21 at 15:30
33

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);
    });
}
doppelgreener
  • 4,809
  • 10
  • 46
  • 63
izupet
  • 1,529
  • 5
  • 20
  • 42
  • 3
    I think this is the better solution. Because @Thierry Templier's solution wil delay all validation rules, not just the async one. – aegyed Oct 20 '16 at 09:48
  • 1
    @n00dl3's solution is more elegant and since rxjs is already available, why not use it to simplify matters more – Boban Stojanovski Apr 30 '18 at 22:27
  • @BobanStojanovski that question refers to angular 2. My solution works only with angular 4+. – n00dl3 Jun 26 '18 at 05:59
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.

Kahlil Lechelt
  • 576
  • 2
  • 11
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • 1
    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 – Chanlito Apr 30 '16 at 12:31
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);
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.

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
  • 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. – funkid Jun 01 '19 at 07:22
4

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')])
});
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 }))
        )
      )
    );
  }
Dima
  • 1,455
  • 1
  • 10
  • 6
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)));
}
V.Ottens
  • 21
  • 3
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

Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
  • 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 `foo+bar@example.dance` 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 Apr 15 '21 at 13:45
  • @doppelgreener thanks for the info, I have updated the solution with a better RegExp – Owen Kelvin Apr 15 '21 at 16:25
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 };
    })
  );
};
Marvin
  • 1,726
  • 1
  • 17
  • 25
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` };
                    })
                )
            )
        )
    }
}
janw
  • 8,758
  • 11
  • 40
  • 62
ale7
  • 1
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();
}
-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>
rkd
  • 27
  • 3
  • 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 – Mustafa Jun 12 '18 at 23:04
  • `this.service.isUsernameReserved(username).subscribe(reserved => this.validated$.next(reserved));` the http call is within the service. – rkd Jun 24 '18 at 21:21