fbpx Skip to content

Aquent | DEV6

Creating an Angular AsyncValidator in a Reactive Form

Written by: Alain Thibodeau

I like working with Angular reactive forms because the Angular team made it relatively straight forward to handle any scenario. One such use case is validating user input against a server in real-time. In this post we will explore one way of doing this using an AsyncValidator in a reactive form. So, let’s get started.

Our form mimics a registration form. It has a first and last name input along with a username. We omit validation in this post for the first and last names, but we want to know if the username is taken before the user can register. The form simply looks like this:

Let’s start off with the template and create a form that points to a formGroup and to a submit method (which we will create later). We are also adding our three inputs for firstName, lastName and username along with a submit button. Notice we’ve assigned a formControlName for each input control and we are also disabling our submit button until the form is validated.

<form [formGroup]="form" (submit)="submitForm()">
   <input type="text" placeholder="First Name" formControlName="firstName"><br/>
   <input type="text" placeholder="Last Name" formControlName="lastName"><br/><br>
   <input type="text" placeholder="Username" formControlName="username"><br/><br>

   <button class="btn btn-sm btn-primary" type="submit" [disabled]="!form.valid">Submit</button>
</form>

Next, let’s create our class for this component with the below (omitted the lines that are not relevant):

export class RegistrationFormComponent  {
    public form: FormGroup;

    constructor(private fb: FormBuilder,
                private http: HttpClient) {

        this.form = this.fb.group({
            'firstName': [''],
            'lastName': [''],
            'userame': ['', [], [UsernameValidator(this.http)]]
        });
    }

    submitForm(): void {
        console.log(this.form.value);
    }

}

Pretty straight forward here, we inject our FormBuilder and our HttpClient in our constructor. Then we create a formGroup with the help of our FormBuilder. Typically, we would have validation on the firstName and lastName, but we omit them to focus on the username validation.

As a reminder, when declaring a form control with the FormBuilder, the first parameter is the default value, followed by synchronous validation and the third is reserved for AsyncValidators.

With that in mind, we create our controls with default values of empty strings, no synchronous validators and only the asynchronous UsernameValidator for the username. Take note that we must pass this validator the HttpClient instance for it to be able to call a service.

The final piece of this registration form is this UsernameValidator. Angular validators are just functions, and AsyncValidators are no different, so let’s see how this looks.

export function UsernameValidator(http: HttpClient): AsyncValidatorFn {
    return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
        return timer(500)
            .pipe(
                switchMap(() => http.get('https://jsonplaceholder.typicode.com/users', {
                        params: { username: control.value }
                    })
                        .pipe(
                            map((user: any[]) => user[0] ? { exists: { user: user } } : null)
                        )
                )
            );
    };
}

For the purpose of this post, we are testing against jsonplaceholder which is a free fake online REST API.

In order for the validator to call out to this service, it needs access to the HttpClient. There are other ways (using static methods) but in this case we pass the http instance to the validator.

The AsyncValidator function needs to return either a Promise or an Observable. We went with an Observable. The Observable stream starts with a timer delay to wait a little bit until the user is done typing. The stream then switches over to calling the http service with the username’s control value as a parameter.

The service returns an array with our user if it found that username, otherwise it returns an empty array. We then use the RxJS map operator to check if we have a user or not and return the appropriate response to the form control.

Remember that we return null if validation passes, and we need to return something if the validation fails. In our case we have access to the user, so we pass it back in the response. We can then not only tell the user that the username exists, but we can even provide a link to their public profile.

The final part is to add the validation results to the template. We can simply show the results by checking if the error exists and displaying the info if it does. So, lets add this part to our template.

<div *ngIf="form.controls['username'].hasError('exists')">
   username exists:
   <pre>{{  form.controls['username'].errors['exists'].user  | json }}</pre>
</div>

For testing, you can use an existing username from the existing users here: https://jsonplaceholder.typicode.com/users

Angular validation, whether it be asynchronous or not, has been well thought out and is relatively straight forward to implement. In production you may not want to display other users’ information, or you may want to call your service differently, but this example should help you to get started.