andreivictor
andreivictor

Reputation: 8451

Angular2 Generic Async Validator in Reactive Forms

I have a reactive form and a form control that must unique:

this.form = new FormGroup({
  name: new FormControl(this.initialValue, [
    Validators.required,
  ], this._uniqueNameValidator.bind(this)),
});

I've wrote an async validator that calls a method from my app service:

private _uniqueNameValidator(control: FormControl) {
    const value = control.value.trim();
    if (value === '' || value === this.initialValue) {
      return of(null);
    }
    control.markAsTouched();
    return timer(500).pipe(
      switchMap(() => {
        return this._appService.validateName(value, this.selectedGroupId);
      }),
      map(response => {
        return response ? {nameTaken: true} : null;
      }),
      catchError(() => {
        return of(null);
      })
    );
 }

The service method is a simple http GET that returns a boolean value:

validateName(name: string, groupId: number): Observable<boolean> {
 // ...
 return this._http.get<boolean>(url);
}

Working example: https://stackblitz.com/edit/angular-8kedup

Now, in my app a have many another forms with controls that must be unique. The uniqueness is checked async, but in other services methods and in other routes. I don't want to copy the code for the validator in every component that needs an async validator for uniqueness.

The problem: I want to write a generic validator, that takes as parameters a service method that must be called in order to determine if the value is unique or not, and the initial form value.

I wrote the following function:

export function UniqueAsyncValidator(fn: any, initialValue: string): AsyncValidatorFn {
  return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
    const value = control.value.trim();
    if (value === '' || value === initialValue) {
      return of(null);
    }
    control.markAsTouched();
    return timer(500).pipe(
      switchMap(() => {
        return fn(value);
      }),
      map(response => {
        return response ? {nameTaken: true} : null;
      }),
      catchError(() => {
        return of(null);
      })
    );
  };
}

And form definition:

    this.form = new FormGroup({
      name: new FormControl(this.initialValue, [
        Validators.required,
        ], UniqueAsyncValidator(this._appService.validateName, this.initialValue).bind(this)),
   });

It seems that the service method is called, but the context (this) in the service method is undefined:

validateName(name: string, groupId: number): Observable<boolean> {
  // ....
  console.log( 'this: ', this );  // -> returns undefined 
  return this._http.get<boolean>(url);
}

Example: https://stackblitz.com/edit/angular-sbbgc8

Thanks!

Upvotes: 2

Views: 733

Answers (1)

Kurt Hamilton
Kurt Hamilton

Reputation: 13515

You're not making the same function call in both versions. Your service function expects groupId. You should call the service with this arg in your callback.

const uniqueValidator = UniqueAsyncValidator(
  value => this._appService.validateName(value, this.selectedGroupId), 
  this.initialValue).bind(this);

this.form = new FormGroup({
  name: new FormControl(this.initialValue, [
    Validators.required,
  ], uniqueValidator)
});

When you create the instance of UniqueAsyncValidator, you are passing in a callback. This allows you to reuse the logic inside the validator, and inject the function that will make the call.

value => this._appService.validateName(value, this.selectedGroupId)

This is arrow function syntax. It is simply declaring an anonymous function that accepts one parameter - value (this is being passed in by your validator) - and returns this._appService.validateName(value, this.selectedGroupId).

DEMO: https://stackblitz.com/edit/angular-ynmzyu

Upvotes: 2

Related Questions