Occasionally, you may want to validate form input against data that is available asynchronous source i.e. a HTTP backend. For instance, checking if a username or email address exists before form submission. In Angular, you achieve this using Async Validators, which we are going to look at in this post.
We are going to build a simple reactive form. It will contain a single form field called username. Then, whenever the user enters their username, we are going to check if it exists and return the response. If the username exists, then the form shall report the following error – The username is already taken.
, as shown below.
Full demo here.
If you are new to Reactive Forms in Angular, please refer to this guide here on getting started.
Without further ado, let’s get started.
Prerequisite
I am going to skip the process of setting up a new Angular Project. Once you have your Angular project setup, in your app module, you will need to import ReactiveFormsModule
from @angular/forms
. This is the only module we require to use Reactive Forms inside our angular application.
@NgModule({
// ...
imports: [
BrowserModule,
ReactiveFormsModule
],
// ...
})
export class AppModule {}
Next, lets add a method for looking up username methods.
Username Lookup Method
First, we are going to add a method to check whether the user’s username exists. In a normal application, this is where you would make a HTTP request to your backend API – either a REST, a GraphQL API etc. But for the purpose of this post, we are going to simulate a HTTP request using RXJS Delay Operator. This will delay our responses by about one second and return an observable just like a HTTP request in Angular.
To achieve this, we are going to create an array of taken usernames - takenUsernames
.
takenUsernames = [
'hello',
'world',
'username'
// ...
];
Then given a username, we are going to determine if it exists within the array using array includes method. After that, whatever the results, we will then delay the response for about a second and return the results as a boolean Observable – Observables<boolean>
.
checkIfUsernameExists(username: string): Observable<boolean> {
return of(this.takenUsernames.includes(username)).pipe(delay(1000));
}
Adding an Async Validator
Next up, we are going to create our async validator. To create this, we just need to implement the AsyncValidatorFn
interface. To implement the AsyncValidatorFN
interface, you need a method that receives a form control class (AKA AbstractControl
) as a parameter. The method then needs to return a promise or an observable of ValidationErrors
or null
. If there are any errors, the method returns ValidationErrors
, otherwise it just returns null.
A ValidationErrors
is an another interface, which is just a key value map of all errors thrown, as shown below:
type ValidationErrors = {
[key: string]: any;
};
For instance, our error for this posts demo shall look something like this { usernameExists: true }
. The key of the returned error allows you to check for specific errors on your form and display them to the user. This allows you to give a more precise feedback to the user instead of generic feedback.
Please note, when there are no errors, you should always return null. If you don’t return null, your Angular Form will be in an invalid state. This could have some undesired side effects.
In our method to check for errors, we will map the boolean response from checkIfUsernameExists
method above to a ValidationErrors
or null response. It will return null if false and a ValidationErrors
object if true.
usernameValidator(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return this.checkIfUsernameExists(control.value).pipe(
map(res => {
// if res is true, username exists, return true
return res ? { usernameExists: true } : null;
// NB: Return null if there is no error
})
);
};
}
We will tack the above method inside our username lookup service.
Adding Async Validators to Our Reactive Form
Now that we have a full working service, we need to use the async validator we just created above. This is a simple matter of adding the array of async validators, after synchronous validators, as shown below.
return this.fb.group({
username: [
null, [Validators.required], [this.usernameService.usernameValidator()]
]
});
Remember to inject the usernameLookupService
into the component you are using it in.
constructor(private fb: FormBuilder, private usernameService: UsernameValidationService) {
// ...
}
UI Implementation
And finally, inside your template, you can check if the form has errors. You can check if a form field has an validation error inside the template as shown below.
frmAsyncValidator.controls['username'].errors?.usernameExists
This returns true if there is an error, else it is undefined, hence the use of safe navigation operator (?)
. And here is the full template for our form:
<form [formGroup]="frmAsyncValidator">
<div class="field has-text-left">
<label class="label">Username </label>
<div [ngClass]="{'is-loading':
frmAsyncValidator.controls['username'].pending }" class="control">
<input [ngClass]="{'is-danger': hasError('username', 'any')}" formControlName="username" class="input is-focused" type="username" placeholder="Your username">
</div>
<div class="has-text-danger" *ngIf="frmAsyncValidator.controls['username'].errors?.usernameExists">
This username is already taken!
</div>
</div>
</form>
A Note on Performance
Currently as we have implemented our async validator, it runs on form value changes. For sync validators, you will likely never notice any performance impact. For async validators however, this can have some undesired side effects. This is because you will most likely be sending HTTP requests to some sort of backend for validation. Running validation on form value changes can end up straining the backend with too many requests.
For this reason, you can change your validators to run onBlur
, instead of on form value changes. You can achieve this by adding the updateOn
property to { updateOn: 'blur' }
for either an individual form control or the entire form:
This is how you can set { updateOn: 'blur' }
property for an entire form:
this.fb.group({/** field here */}, { updateOn: 'blur' });
And this is how you do it for an individual form control:
username: [
null,
{
validators: [Validators.required],
asyncValidators: [this.usernameService.usernameValidator()],
updateOn: 'blur'
}
]
Conclusion
That’s it for me in this post, you can find the code for this posts example here on GitHub. If you have any questions/issue/suggestion, feel free to use the comment section below. You can also join me on my new Slack channel here or on Twitter where am available to help in any way I can.
Thank you.
Top comments (0)