2

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!

2
  • Second link is broken Commented Mar 31, 2020 at 7:47
  • 1
    i've fixed it now Commented Mar 31, 2020 at 7:47

1 Answer 1

2

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

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

5 Comments

thanks for your reply, it seems it works. but I don't really understand the value => this._appService.validateName(value, this.selectedGroupId). Can you please explain it a little bit?
You will be creating an instance of UniqueAsyncValidator every time you want to use it. You could theoretically have a unique callback each time. If this is the case, then you just pass in that unique callback at the point of constructing it. This is the point of a callback?
I will add an explanation to my answer
I would recommend adding a type to fn - this makes it clear what you are dealing with. fn: (value: string) => Observable<boolean>. It's a function that accepts 1 string, and returns an observable boolean
And if you are always passing in an arrow function, you don't need .bind(), and you can just call fn(value) inside the validator. But what you have is fine, I'm just giving you options :)

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.