How to add debounce time to an async validator in angular 2?

AngularValidationAsynchronousDebouncing

Angular Problem Overview


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

Angular Solutions


Solution 1 - Angular

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.

Solution 2 - Angular

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
}

Solution 3 - Angular

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.

Solution 4 - Angular

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

Solution 5 - Angular

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.

Solution 6 - Angular

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

Solution 7 - Angular

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.

Solution 8 - Angular

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

Solution 9 - Angular

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

Solution 10 - Angular

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

Solution 11 - Angular

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

Solution 12 - Angular

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

Solution 13 - Angular

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

Solution 14 - Angular

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>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionChanlitoView Question on Stackoverflow
Solution 1 - Angularn00dl3View Answer on Stackoverflow
Solution 2 - AngularPavelView Answer on Stackoverflow
Solution 3 - AngularAndrew PhilipsView Answer on Stackoverflow
Solution 4 - AngularizupetView Answer on Stackoverflow
Solution 5 - AngularThierry TemplierView Answer on Stackoverflow
Solution 6 - AngularAlberto AldegheriView Answer on Stackoverflow
Solution 7 - AngularWilli MentzelView Answer on Stackoverflow
Solution 8 - AngularДніщенко ДенисView Answer on Stackoverflow
Solution 9 - AngularDimaView Answer on Stackoverflow
Solution 10 - AngularV.OttensView Answer on Stackoverflow
Solution 11 - AngularOwen KelvinView Answer on Stackoverflow
Solution 12 - AngularMarvinView Answer on Stackoverflow
Solution 13 - Angularale7View Answer on Stackoverflow
Solution 14 - AngularrkdView Answer on Stackoverflow