Angular custom async validators can be very useful for checking data against a backend server, such as a JSON http service. Async validators are only executed after synchronous validators are marked as valid, which helps to reduce unnecessary calls to the server.
In addition, the call to the server can be timed to only happen after a specified period since the last keystroke, thus simulating the effect of a debounce using the RxJS operator debounceTime(). This delay can be programmed into the Validator function itself, or into the service called by the validator.
Although there are lots of examples of implementing custom async validators in Angular, I found implementing them in Ionic 4 to be tricky, and the purpose of this tutorial is to demonstrate the technique needed for Ionic 4. I start with a basic registration form using simple Synchronous Validators and then show the 4 steps needed to add custom Async Validators to the form when validation is performed against a JSON web server.
Full source code is shown below, so you can code along, and a Github repository is also available if you wish to go straight to the finished app. No special requirements are needed, apart from the installation of Ionic 4.
When to use Async Validators?
Async validators are used when you want to verify data against an external data source. A very common example is a user registration form where you want to check if the user is already registered. Users might be identified by their email address, their user name, and/or their display name and it’s a good idea to check while a new user is completing a registration form whether the values they enter are already used. The values could be validated when the user submits their registration form, but it is a better user experience if they get immediate feedback if one of their values is not available.
What are Synchronous Validators?
Synchronous Validators are built into Angular/Ionic and provide immediate feedback to the user when their data is invalid. Common Synchronous validators are Required, MinLength, MaxLength and Pattern where the value must conform to a regular expression.
Although Ionic 4 makes use of Angular 7, by default, I found it very difficult to get Angular async validators to work in Ionic 4.
There are quite a few examples of using an Angular Async Validator, such as …
Angular Docs - implementing custom async validator
Youtube Videos - Angular Reactive Forms Async Validation
Fabio Biondi - Angular Reactive Forms Custom Async Validators
Tomasz Sochacki - How to do asynchronous validation in Angular 7
These last two in particular helped me get async validators to work, simply, in my Ionic 4 application and the following notes explain the process used.
In this example I’m going to emulate a website registration form using Ionic 4 and an Angular Reactive Form. The example assumes you have Ionic 4 installed already, so to follow along create a new Ionic 4 project using the Blank template, I’ve called mine ionic4AsyncVal, but you can use any name you like.
Basic Registration Form with Synchronous Validators
Let’s start with a basic Ionic 4 Reactive registration form with just a User Name and Display Name. (I could have used email address, or any other fields, it doesn’t matter for this demonstration)
Update your home.page.ts with the following code…
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
public registerForm: FormGroup;
constructor()
{
this.registerForm = new FormGroup({
userName: new FormControl(null, [Validators.required, Validators.minLength(4)]),
displayName: new FormControl(null, [Validators.required, Validators.minLength(4)]),
});
}
// use getters to simplify control names in the html form
// prefix with g so it is clear where they are used
get gUserName() { return this.registerForm.controls["userName"]; }
get gDisplayName() { return this.registerForm.controls["displayName"]; }
onRegister() {
console.log(this.registerForm.value);
if (this.registerForm.invalid) {
console.log('form invalid, not submitted');
return;
}
console.log('submitting the form to the server');
}
}
I’ve specified a “getter” for each of our controls, to make them easier to reference in our html, and added an onRegister() method to console.log() the values entered and whether the form is valid.
In our constructor I created an Angular Reactive form with two controls, no default values, and two Synchronous Validators - required, and minLength(4). I’m using a Reactive form so I need to modify home.module.ts by importing the ReactiveFormsModule…
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { HomePage } from './home.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
IonicModule,
RouterModule.forChild([ { path: '', component: HomePage } ])
],
declarations: [HomePage]
})
export class HomePageModule {}
Next, update home.page.html with the following code for our registration form…
<ion-header>
<ion-toolbar>
<ion-title>
Registration Form
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-grid>
<ion-row justify-content-center>
<ion-col align-self-center size-xl="4" size-lg="6" size-md="9" size-xs="12">
<ion-card>
<ion-item color="primary" text-center>
<ion-label class="header">Registration Form</ion-label>
</ion-item>
<ion-card-content>
<form [formGroup]="registerForm" (ngSubmit)="onRegister()">
<ion-item>
<ion-label position="floating">User Name*</ion-label>
<ion-input formControlName="userName" type="text" required></ion-input>
</ion-item>
<ion-label *ngIf="gUserName.hasError('required')" color="danger" position="stacked">User Name is Required</ion-label>
<ion-label *ngIf="gUserName.hasError('minlength')" color="danger" position="stacked">User Name must be at least 4 characters</ion-label>
<ion-item>
<ion-label position="floating">Display Name*</ion-label>
<ion-input formControlName="displayName" type="text" required></ion-input>
</ion-item>
<ion-label *ngIf="gDisplayName.hasError('required')" color="danger" position="stacked">Display Name is Required</ion-label>
<ion-label *ngIf="gDisplayName.hasError('minlength')" color="danger" position="stacked">Display Name must be at least 4 characters</ion-label>
<ion-row padding-top>
<ion-col>
<ion-button [color]="registerForm.invalid ? 'warning' : 'success' " expand="full" type="submit">
Register
</ion-button>
<ion-item lines="none">
<ion-label size="small">* Required fields</ion-label>
</ion-item>
</ion-col>
</ion-row>
</form>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
The registration form is set up now with some basic Synchronous Validators, a function to handle the submission, and some Ionic 4 markup to make the form look reasonable in either a mobile, or web app. I’ve added error messages for each of the validators, and added some Ionic 4 markup to colorcode the submit button according to whether the form is valid.
Test the form at this stage by running Ionic Serve and you should see a form like this…
Adding Async Validation
In this registration form I want to make sure that the user name, and display name, etc are not already used. The way to check that is to search our backend database of registered users, and we do that by submitting a http request to our web server and getting back a JSON response that tells us if the value has been used. For this example I’m going to use a publicly accessible JSON server that already has some test User Data…
Public JSON test server: JSON Placeholder Typicode
…and I can search it with a simple http GET request, to find a User Name (?username=value to find)…
https://jsonplaceholder.typicode.com/users?username=Antonette
…or a Display Name (?name=value to find)…
https://jsonplaceholder.typicode.com/users?name=Patricia Lebsack
If the value is not found the server returns an empty array [].
4 steps to implement Asynchronous Validation.
- Create a service to call the webserver and test the entered value.
- Create an Async Validator function, to call the service.
- Add the Async Validator function to the Reactive Form.
- Update the error messages in the HTML page.
1. Create a Validator Service
Run the command…
ionic g service services/user
.. to create a http service. I called it UserService because my registration form belongs to User management and I want all my User services in the same file. You might want to put your validation services in a Validation service file - that is up to you.
Now, update the services/user.service.ts file with the following code…
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
const URL = 'https://jsonplaceholder.typicode.com/users';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(
private http: HttpClient
) { }
//---------------------------------------------------
isUserNameUnique(val: string): Observable {
console.log('Going to the server:',val);
return this.http.get(`${URL}?username=${val}`)
}
//---------------------------------------------------
isDisplayNameUnique(val: string): Promise {
console.log('Going to the server:',val);
return new Promise(resolve => {
this.http.get(`${URL}/?name=${val}`)
.subscribe(res => {
console.log('res: ', res);
if (res.length) {
resolve({ unique: false });
} else {
resolve({ unique: true });
}
})
})
}
//---------------------------------------------------
}
The function isUserNameUnique calls the HTTP request and returns to the Validator Function where the response will need to be evaluated.
The function isDisplayNameUnique calls the HTTP request and evaluates the response before returning an object, via a Promise, to the validator function.
Either technique is OK, validators can use Observables or Promises as return values from a service.
Also, update app.module.ts to include an import for HttpClientModule, partial code only shown…
import { HttpClientModule } from '@angular/common/http';
...
...
imports: [
...
...
HttpClientModule
],
...
2. The Async Validator Function
The next step is to create the validator function itself, using the technique described by Tomasz Sochacki - How to do asynchronous validation in Angular 7 , since this is the only technique I’ve found that works in Ionic 4!
So, create a new folder, and file called validators/unique_user.ts, containing the following code…
import { timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { FormControl } from '@angular/forms';
import { UserService } from '../services/user.service';
//-------------------------------------------------------
export const uniqueUserName = (userService: UserService, timeDelay: number = 1000) => {
return (control: FormControl) => {
return timer(timeDelay).pipe(
switchMap(() => userService.isUserNameUnique(control.value)),
map(res => {
control.setErrors(null);
if( res.length) {
return (!res[0].id) ? null : { notUnique: true };
} else {
return null;
}
})
);
};
};
//-------------------------------------------------------
export const uniqueDisplayName = (userService: UserService, timeDelay: number = 1000) => {
return (control: FormControl) => {
return timer(timeDelay).pipe(
switchMap(() => userService.isDisplayNameUnique(control.value)),
map(res => {
control.setErrors(null);
return res.notUnique ? { notUnique: true } : null;
})
);
};
};
This file contains two Async Validators to match the two validation services. The UserService is imported to avoid errors in the editor, and is passed to the validator as a parameter when calling the validator from the Reactive form.
The validators are declared as const, which I believe is the secret to getting these to work in Ionic 4?
The code supplied by Tomasz Sochacki has also used a timer, set with the timeDelay parameter and switchMap to ensure that the service functions are only called after the specified period has passed since the last keystroke.
The uniqueUserName validator evaluates the response from the service, whereas in the uniqueDisplayName the response from the service is evaluated by the service, and is returned directly by the validator.
I have named the value returned by the validator as “notUnique” since this will be used by the Reactive form as a Validation Error so makes sense to me!
Warning, don’t try returning { notUnique: false } instead of “null” when there are no errors, the Reactive Form will evaluate this as Invalid.
3. Add the Async Validators to the Reactive Form Controls
Now, update the home.page.ts code with the following…
(i) Add new imports…
import { UserService } from '../services/user.service';
import { uniqueUserName, uniqueDisplayName } from '../validators/unique_user';
(ii) Update the constructor and Reactive form…
constructor(
private userService: UserService
)
{
this.registerForm = new FormGroup({
userName: new FormControl(
null,
[Validators.required, Validators.minLength(4)],
[uniqueUserName(this.userService, 1000)]
),
displayName: new FormControl(
null,
{
validators: [Validators.required, Validators.minLength(4)],
asyncValidators: [uniqueDisplayName(this.userService, 0)],
updateOn: 'blur'
}
)
});
}
Async Validators are the third parameter of Reactive Form controls and the userName control definition now calls [uniqueUserName(this.userService, 1000)] as the Async Validator, passing this.userService as the name of the service to use in the validator, and 1000 as the timeDelay parameter to be used by the validator.
The definition of the displayName control is declared differently, with validators passed as an object, in which the property updateOn: ‘blur’ is used to signify that the validation is only performed on blur. This means that the validation is only called when the control loses focus, rather than after every keystroke, so the uniqueDisplayName(this.UserService, 0) doesn’t need a timeDelay when calling the validation service, so 0 is passed as the timeDelay parameter.
4. Display Async Error Messages
Update your home.page.html file with the following code…
<ion-header>
<ion-toolbar>
<ion-title>
Registration Form
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-grid>
<ion-row justify-content-center>
<ion-col align-self-center size-xl="4" size-lg="6" size-md="9" size-xs="12">
<ion-card>
<ion-item color="primary" text-center>
<ion-label class="header">Registration Form</ion-label>
</ion-item>
<ion-card-content>
<form [formGroup]="registerForm" (ngSubmit)="onRegister()">
<ion-item>
<ion-label position="floating">User Name*</ion-label>
<ion-input formControlName="userName" type="text" required></ion-input>
</ion-item>
<ion-label *ngIf="gUserName.hasError('required')" color="danger" position="stacked">User Name is Required</ion-label>
<ion-label *ngIf="gUserName.hasError('minlength')" color="danger" position="stacked">User Name must be at least 4 characters</ion-label>
<ion-label *ngIf="gUserName.hasError('notUnique')" color="danger" position="stacked">User Name is already taken</ion-label>
<ion-label *ngIf="gUserName.pending" color="warning" position="stacked">Please wait, checking User Name</ion-label>
<ion-item>
<ion-label position="floating">Display Name*</ion-label>
<ion-input formControlName="displayName" type="text" required></ion-input>
</ion-item>
<ion-label *ngIf="gDisplayName.hasError('required')" color="danger" position="stacked">Display Name is Required</ion-label>
<ion-label *ngIf="gDisplayName.hasError('minlength')" color="danger" position="stacked">Display Name must be at least 4 characters</ion-label>
<ion-label *ngIf="gDisplayName.hasError('notUnique')" color="danger" position="stacked">User Name is already taken</ion-label>
<ion-label *ngIf="gDisplayName.pending" color="warning" position="stacked">Please wait, checking Display Name</ion-label>
<ion-row padding-top>
<ion-col>
<ion-button [color]="registerForm.invalid ? 'warning' : 'success' " expand="full" type="submit">
Register
</ion-button>
<ion-item lines="none">
<ion-label size="small">* Required fields</ion-label>
</ion-item>
</ion-col>
</ion-row>
</form>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
Here I’ve added checks for…
<ion-label *ngIf="gUserName.hasError('notUnique')" color="danger" position="stacked">User Name is already taken</ion-label>
…to display an error message to the user if the Async Validation “notUnique” is set.
Similarly, because the Async Validation can take some time to get a response from the server, I’ve added a test for “pending” and display a warning message to the user…
<ion-label *ngIf="gUserName.pending" color="warning" position="stacked">Please wait, checking User Name</ion-label>
Conclusion
Async Validators are a useful addition to Ionic 4 Reactive Forms and can be implemented relatively easily with 4 steps, resulting in a better user experience. The example is available as a Github repository.