What is a validator in Angular?
First, let's talk about Validators. What is a validator, and how we can use them to improve our forms.
Ocasionally, we want to validate some input in our fields before submission, which will be validated against an asynchronous source. For instance, you may want to check if a label or some data exists before submission. In Angular, you can do this using Async Validators.
Create a basic application
We are going to create a very minimalist form that does one thing: check if the username
already exists. In this case, we are going to use a mix of async and sync validators to accomplish our task.
It's very common, in most registration forms, to require an unique username. Using an API call, we to capture if that username is already in use according to the database.
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
constructor(private fb: FormBuilder) {}
registrationForm = this.fb.group({
name: [null, [Validators.minLength(3), Validators.required]],
username: [null, [Validators.minLength(3), Validators.required]],
});
}
Created a basic form that has a FormGroup
called registrationForm, with 2 FormControl. Also, I added 2 sync validators for make the input required and also to check if the input has a minimum length, using Validators.required
and Validators.minLength
in each case.
<p>Using Custom Async Validators</p>
<div class="container" [formGroup]="registrationForm">
<mat-form-field class="example-full-width">
<mat-label>Name</mat-label>
<input matInput placeholder="Ex. John Doe" formControlName="name" />
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Username</mat-label>
<input matInput placeholder="Ex. Batman" formControlName="username" />
</mat-form-field>
<div class="button-container">
<button
mat-raised-button
color="primary"
[disabled]="!registrationForm.valid"
>
Submit
</button>
</div>
</div>
I'm using @angular/material
just to simplify our styles, and because it provides a clean way to display errors using the mat-error
component. This component will be shown to the user if the required error is present. The button also checks if the FormGroup
is valid, otherwise it will be disabled.
Async validators
Our current app works pretty well, and in most cases, will meet all of the requirements. But what if we want to make sure that the username is unique before allowing the user to submit their information?. Creating an Async Validator
could be simple. Let's find how to create it, and add it to our current form.
import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class UserService {
private existingUsernames = ['Batman', 'Superman', 'Joker', 'Luthor'];
checkIfUsernameExists(value: string) {
return of(this.existingUsernames.some((a) => a === value)).pipe(
delay(1000)
);
}
}
Our method to check if the username already exists is called checkIfUsernameExists
. It returns an observable with a 5 seconds delay to simulate a very slow API call. Now, we can create our Async Validator to check if the username exists against that method.
import {
AbstractControl,
AsyncValidatorFn,
ValidationErrors,
} from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { UserService } from './user.service';
export class UsernameValidator {
static createValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors> => {
return userService
.checkIfUsernameExists(control.value)
.pipe(
map((result: boolean) =>
result ? { usernameAlreadyExists: true } : null
)
);
};
}
}
Our UsernameValidator
class takes our UserService as an argument. This method returns a AsyncValidatorFn
which receives the FormControl
that is placed on, providing us access to the current value.
An AsyncValidatorFn
must return either a promise or an Observable of type ValidationErrors
.
We use the RxJS map operator to check the value emitted, and either return null if the user doesn't exist, or return a ValidationError with an error type of usernameAlreadyExists
.
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { UserService } from './user.service';
import { UsernameValidator } from './username-validator';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
constructor(private fb: FormBuilder, private userService: UserService) {}
registrationForm = this.fb.group({
name: [null, [Validators.minLength(3), Validators.required]],
username: [
null,
[Validators.minLength(3), Validators.required],
[UsernameValidator.createValidator(this.userService)],
],
});
}
We import our UsernameValidator
and UserService
into our component, and declare in the constructor component.
The UsernameValidator
is added to our FormControl
as the third parameter, calling the createValidator method, and passing a reference to the UserService
.
<mat-form-field class="example-full-width">
<mat-label>Username</mat-label>
<input matInput placeholder="Ex. Batman" formControlName="username" />
<mat-error
*ngIf="registrationForm.get('username').hasError('usernameAlreadyExists')"
>
Username already <strong>exists</strong>
</mat-error>
</mat-form-field>
We update our template to check for an additional error called usernameAlreadyExists
, so if our UserService
finds that the username already exists, it will provide a
The validation status of the control. There are four possible validation status values:
VALID: This control has passed all validation checks.
INVALID: This control has failed at least one validation check.
PENDING: This control is in the midst of conducting a validation check.
DISABLED: This control is exempt from validation checks
Let's see StackBlitz in action.
Conclusion
Creating beautiful forms for our web app might seem simple, but it's typically more complicated than one would expect. Sometimes, we also need to verify the information, because we don't want to blindly send data to the backend, and then reject it. This can lead to a poor experience for users and result in low adoption rates.
Creating an AsyncValidator
can help us improve the user-experience, and also avoid sending data to the backend, resulting in a rejection.