Angular: Showing Loading Spinner and Error Messages with Custom Async Validator in Reactive Forms

AngularEnthusiast
Geek Culture
Published in
8 min readNov 24, 2022

--

Almost everyone of us have an account on Google. While creating an account, you must have encountered the below error message. This is an example of async validation, where we are validating the Username field via an asynchronous HTTP call to the server before the form is submitted.

Inspired by this example, I have created a small angular project which demonstrates the below points:

  1. Asynchronous Validators execute only after Synchronous Validators pass.
  2. Displaying a loading spinner when the Asynchronous Validator’s execution is in progress.
  3. Display error messages for both Synchronous and Asynchronous Validators.

The application is as simple as the below screenshot. All we have is a text box for filling the Username field and a “Create” button which actually does nothing in this example. The purpose of adding the button is to show how it is disabled/enabled during the sync/async validations.

AppComponent Template:

  1. The template contains a root FormGroup asyncForm which has 1 FormControl username.

2. Also we have a“Create” button which is disabled when the asyncForm has an INVALID status or when the username FormControl is in PENDING state. A FormControl is in PENDING state when the asynchronous validation is in progress.

3. We are displaying the sync/async error messages(if any) in the template.

4. Finally we are also displaying a loading spinner next to the text box when the asynchronous validation is in progress.

Moving to the AppComponent Class:

Starting with the ngOnInit lifecycle.

  1. Below is the asyncForm structure.
this.asyncForm = this.fb.group({
username: [‘’,
[Validators.required, GenericValidator.syncUsernameValidator(3, 10)],
this.usernameValidator.validate.bind(this.usernameValidator),
],
});

username is the FormControl which has 2 synchronous validators: Validators.required is built-in and syncUserNameValidator is the custom one we have defined.

It also has 1 asynchronous validator: userNameValidator.

2. Lets move to the Sync and Async Validator implementation.

Below is the Sync Validator which just checks the min and max character length are as expected. This can be acheived using the build-in Validators.minlength and Validators.maxlength. But i wanted to include a built-in, custom sync and custom async validator to demonstrate how the 3 behave together.

DataService:

Before moving to the custom Async Validator, first lets check the DataService class below. In this service, we are calling the Fake JSON Placeholder API to verify if the username entered in the textbox matches the username of either of the 10 users in their database.

If there is a match i.e if the response from the API has data, then we are returning an observable emitting true.

If there are no matches i.e the username entered in the textbox is unique and the API response is empty,then we are returning an observable emitting false.

If there are any errors while making the API call, we are just throwing back an observable of the error.

The custom Async validator is just a service class which implements the AsyncValidator interface. This interface requires that validate() must be implemented inside the class.

The validate() returns an Observable or Promise of ValidationErrors.

Based on the value returned by the validateUserName() in the DataService, we either return a ValidationError object {duplicateUser:true} or null.

The object is returned in case the username entered in the textbox has a match to one of the 10 users in the database. In case of no match, null is returned.

In case of any error encountered during API call, we are returning a ValidationError object {validationApiError:err.message}.

3. Let’s now check all the scenarios.

Getting back to the objectives we listed earlier.

=>Asynchronous Validators execute only after Synchronous Validators pass.

As you can see in the Async Validator code highlighted below, that we are logging a message when it executes. So the Async validator will execute only after the below Sync Validations pass.

  1. The Username field must be filled.
  2. Username must have atleast 3 characters and a max of 10 characters.

I initially enter only 2 characters in the Username field. Please observe the error message displayed below the textbox and the message logged in the console. The built-in validator has passed but the Custom Sync Validator has failed. We can see no message stating that the Async Validator is executing.

Now let me delete the 2 characters I have entered. We see 2 error messages under the textbox which imply that both the built-in and Custom Sync validations have failed. Again no message logged in console which would indicate the execution of Async Validator.

Now I have entered “Bret” in the Username field and the Async Validator starts executing. The Async Validator has executed twice- once for the string “Bre” and next for the string “Bret”. Since the username “Bret” already exists in the Fake JSON Placeholder Database,we have displayed the error message.

Note that the Create Button is disabled as well.

But has the API call executed successfully for both the executions of the Async Validator? Lets check the Network tab. The API call for “Bre” has got cancelled and only the API call for “Bret” has succeeded. This may not happen all the time. It depends on how fast/slow the user types.

If the user types “Bre” at once, takes some time and then types “t”, then its possible that the inner observable(Http Request) for “Bre” can complete execution before the next inner observable(Http Request) for “Bret” begins. In that case there is a possibility that both API calls may succeed.

=>Displaying a loading spinner when the Async Validator execution is in progress.

When the Async Validator executes, the FormControl username is in PENDING state. We use this to our advantage and display a spinner.

<span class=”spinner-border col-md-1"
*ngIf=”asyncForm.get(‘username’).pending”></span>
Sync Validator failing
Async Validator executing with spinner

Let’s examine the scenario when the API doesnt succeed or gets cancelled. It fails for any other reason. To check this scenario, I have updated the endpoint in the DataService to a non-existent one.

We have displayed the exact error message under the textbox as you can see below. The Create button remains disabled.

=> Finally we move to the display of error messages for both Sync and Async Validators.

I really didnt want to crowd the template with multiple ngIf’s for every validation error type. Instead I moved this logic to a small method in the class so that the template looks clean like below. Just a single line of code to display all error messages associated with the username FormControl.

<span class=”error” *ngIf=”errorMap.username.length”>{{errorMap.username}}
</span>

We want the validation error messages to display everytime the value/status of the username FormControl changes but with a debounce of 2 secs.

Why do we look for status changes? We really dont require it if only synchronous validators are defined on the FormControl. But since, we also have an asynchronous validator, we require to check the changes in the status of the FormControl to monitor the progress in the asynchronous validator execution.

combineLatest(
this.asyncForm.get(‘username’).valueChanges,
this.asyncForm.get(‘username’).statusChanges
)
.pipe(debounceTime(2000))
.subscribe((result) => {
console.log(‘[Value of Control,Status of Control]:’, result);
this.setValidationMessage(this.asyncForm.get(‘username’), ‘username’);
});

We have input the valueChanges and statusChanges observables to the combineLatest operator. Every time there is a change in either value/status, the setValidationMessage() is called. We are passing the username FormControl and the control name as arguments.

Before we proceed to setValidationMessage(), lets see the below 2 properties in the class.

errorMap = {
username: ‘’,
};

validationMessages = {
username: {
required: ‘Username is required’,
minlength: ‘Username must have atleast 3 characters’,
maxlength: ‘Username can have a max of 10 characters’,
duplicateUser: ‘Username already exists ! Please choose another Username’,
},
};

errorMap property is an object which contains the names of all FormControls(in the example:username is the only FormControl) as keys. The value to all the keys will be an empty string.

Later the value to the key will be set to an error message. We are using this errorMap property to display errors related to any FormControl in the template as below.

<span class=”error” *ngIf=”errorMap.username.length”>{{errorMap.username}}
</span>

validationMessages property is an object which contains the names of all FormControls as keys. The value corresponding to each key is another object. The properties of this object are all the “error names/types” possible for the FormControl key.

As you can see below, the username FormControl is a key, whose value is an object of error names/types. Corresponding to each error name/type, we have specified the error message to display. These error messages might look familiar.

validationMessages = {
username: {
required: ‘Username is required’,
minlength: ‘Username must have atleast 3 characters’,
maxlength: ‘Username can have a max of 10 characters’,
duplicateUser: ‘Username already exists ! Please choose another Username’,
},
};

Let’s now move to the setValidationMessage().

setValidationMessage(c: AbstractControl, controlName: string) {
this.errorMap[controlName] = ‘’;

if (c.errors) {
this.errorMap[controlName] = Object.keys(c.errors).map((key: string) => {

if (key === ‘validationApiError’) {
return c.errors[key];
} else {
return this.validationMessages[controlName][key];
}
})
.join(‘ ; ‘);
}
}
  1. Since there is only 1 FormControl username, it is obvious that argument c is the FormControl and controlName is “username”.

We are resetting the “username” property of the errorMap property object to “” to clear any existing error messages.

this.errorMap[controlName] = ‘’;

2. If the username FormControl has any errors, we extract the keys of the errors property object on the FormControl into an array of keys using Object.keys. Each key is nothing but the error name/type eg: required, minlength, maxlength etc.

Next we apply the map operator on the array of keys. If the key name is anything other than “validationApiError”, we directly extract the error message for that key from the validationMessages property.

We have not included the “validationApiError” in the validationMessages property because we cannot predict the error message for an API failure. It could be for any reason.

Hence for the “validationApiError” error type, we pick the message from the errors property on the FormControl itself.

3. Finally we are applying the join() on the result of the map operator to combine all the error messages corresponding to the error types with a semicolon.

Please check the below screenshot to see how the error messages for 2 error types: required and minlength are combined using a semicolon.

You can find the entire working example below.

--

--

AngularEnthusiast
Geek Culture

Loves Angular and Node. I wish to target issues that front end developers struggle with the most.