2

I've created a custom Async validator that uses a service to validate emails against a server. However, this means the server is hit every time a character is entered which is no good. I've followed several answers on here that I haven't been able to get working.

My Validator:

import {FormControl, NG_ASYNC_VALIDATORS, Validator} from 
'@angular/forms';
import { Http } from '@angular/http';
import {Directive, forwardRef} from "@angular/core";
import {ValidateEmailService} from "../services/validate-email.service";
import {UserService} from "../services/user.service";

@Directive({
  selector: '[appEmailValidator]',
  providers: [
    { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => EmailValidator), multi: true }
  ]
})
export class EmailValidator implements Validator {
  public validateEmailService: ValidateEmailService;

  constructor(
    private _http: Http,
    private _userService: UserService
  ) {
    this.validateEmailService = new ValidateEmailService(this._http, this._userService);
  }

  validate(c: FormControl) {
    return new Promise(resolve => {
      this.validateEmailService.validateEmail(c.value)
        .subscribe((res) => {
          console.log(res);
          if (res.valid) {
            resolve(null);
          } else {
            resolve({
              valid: {
                valid: false
              }
            });
          }
        });
      })
    }
}

It works well by itself but as soon as I try to add some form of debounce to it, I end up breaking it.

I've tried the answers from this question and I get errors along the lines of Type X is not assignable to type 'Observable<any>' etc.

I got close by using a setTimeout but all that ended up doing was halting the functionality.

My end goal is to only run the validator when the input hasn't been changed for about 600 ms, but would settle for only validating once every 600-2000 ms.

For additional clarity, the validateEmail method from the ValidateEmailService:

public validateEmail(email: string) {

  let validateEmail = new ValidateEmail(email);

  return this._http.get(
    this.getUrl(validateEmail),
    this.getOptionArgs())
    .map((response: Response) => Object.assign(new UserEmailVerification(), response.json().UserEmailVerification));

}

2 Answers 2

3

I haven't seen an async validator implemented as a Directive, but rather as a validator function assigned to a form control.

Here's an example validator I use for a similar case:

import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms';
import { Observable, timer, of } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';

import { MembershipsService } from '@app/memberships/memberships.service';

@Injectable()
export class MembershipsValidators {

  constructor (
    private membershipsService: MembershipsService,
  ) {}

  checkMembershipExists(email?: string): AsyncValidatorFn {
    return (control: AbstractControl): Observable<{ [key: string]: any } | null> => {
      if (control.value === null || control.value.length === 0) {
        return of(null);
      }
      else if (email && email === control.value) {
        return of(null);
      }
      else {
        return timer(500).pipe(
          switchMap(() => {
            return this.membershipsService.lookupMember(control.value).pipe(
              map(member => {
                if (!member) {
                  return { noMembership: { value: control.value } };
                }

                return null;
              })
            );
          })
        );
      }
    };
  }

}

That gets imported and applied to a form control as such:

this.form = this.formBuilder.group({
  memberEmail: new FormControl('', {
    validators: [ Validators.required, Validators.pattern(regexPatterns.email) ],
    asyncValidators: [ this.membershipsValidators.checkMembershipExists() ],
  }),
});

This way, the async validator doesn't fire until the synchronous validators are satisfied.

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

13 Comments

I'm somewhat new to angular, so this may be wrong, but the Directive is because I'm using this on a template-driven form rather than a reactive form. Unfortunately I'm stuck using a template-driven form for the time being.
I see. The same methodology should apply. The main difference is that I'm returning an observable instead of a promise, but only after 500 milliseconds have passed.
@Brandon that's overkill for my requirements. I fixed this by simply checking for the PENDING state on the FormGroup itself. Using Reactive Forms, Angular emits events on the FormGroup, you can easily show/hide an indicator if the form is in the PENDING state. Thank you.
@Nexus Interesting. Thanks for the info. Amazing that after 24 years of web development, I still learn something new on pretty much a daily basis :)
@nomadoda Yes. Each time the value changes, the validator is going to execute. The timer is going to limit the call to . lookupMember() to the specified interval. By using switchMap, lookupMember will be cancelled and fired again with the new value if it's still in progress.
|
3

You could create an Observable within your promise to accomplish the debounce.

This logic may not be cut and paste but should get you close.

import {distinctUntilChanged, debounceTime, switchMap} from 'rxjs/operators';

 validate(c: FormControl) {
  return new Promise(resolve => {
    new Observable(observer => observer.next(c.value)).pipe(
      debounceTime(600),
      distinctUntilChanged(),
      switchMap((value) => { return this.validateEmailService.validateEmail(value) })
    ).subscribe(
      (res) => {
        console.log(res);
        if (res.valid) {
          resolve(null);
        } else {
          resolve({
            valid: {
              valid: false
            }
          });
        }
      }
    )
  })
}

16 Comments

I'm getting an error "cannot find name 'tap'". Any ideas?
Add this to your component. import { tap } from 'rxjs/operators'
I edited the answer to include all the imports you would need at the top of code example.
Getting closer! On the tap line, the second value - validateEmail(value) - is throwing me an error: Argument of type '{}' is not assignable to parameter type 'string'
I would need to see your validateEmail function in your validateEmailService. I suspect the function is not happy with the format being passed to it as the argument.
|

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.