Angular Reactive Forms with Synchronous and Asynchronous Validators
Synchronous and Asynchronous Validators
Here in this post I will show you how to work with synchronous and asynchronous validators in Angular reactive forms.
Synchronous Validators
In Angular, a synchronous validator is a function used in reactive forms that immediately returns a validation result. Synchronous validators in Angular perform immediate checks on the control’s value.
Key characteristics of synchronous validators are:
- immediate return where they take a
FormControlinstance as an argument and directly return either null (if the value is valid) or aValidationErrorsobject (if there are validation errors). - built-in validators such as
Validators.required,Validators.email,Validators.minLength, andValidators.maxLength, etc.
The diagram for the synchronous validation is given below:
Component Class
The component class file app.ts defines the required validators.
import { Component, signal, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.html',
standalone: false,
styleUrl: './app.css'
})
export class App implements OnInit {
protected readonly title = signal('Angular Reactive Forms');
registrationForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.registrationForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
address: ['', [Validators.required, Validators.minLength(30)]],
country: ['', [Validators.required, Validators.minLength(2)]],
pin: ['', [Validators.required, Validators.minLength(5), Validators.pattern(/^\d{6}$/)]]
});
}
onSubmit() {
if (this.registrationForm.valid) {
console.log('Form Submitted!', this.registrationForm.value);
} else {
console.log('Form is invalid.');
}
}
}
Synchronous validators are passed as the second argument to FormControl (or FormBuilder.control). The examples of synchronous validators include Validators.required, Validators.minLength, Validators.email, Validators.pattern. These validators validate the fields immediately.
Template – Reactive Form
The following template app.html declares the following form. The form will show the feedback messages if the necessary validations are failed.
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
<div>
<label>
Full Name:
<input id="name" formControlName="name" placeholder="Full name">
</label>
<div *ngIf="registrationForm.get('name')?.invalid && (registrationForm.get('name')?.dirty || registrationForm.get('name')?.touched)">
<div *ngIf="registrationForm.get('name')?.errors?.['required']">Please provide a full name.</div>
<div *ngIf="registrationForm.get('name')?.errors?.['minlength']">Name must be at least 3 characters.</div>
</div>
</div>
<div>
<label>
Email:
<input id="email" formControlName="email" placeholder="Your email">
</label>
<div *ngIf="registrationForm.get('email')?.invalid && (registrationForm.get('email')?.dirty || registrationForm.get('email')?.touched)">
<div *ngIf="registrationForm.get('email')?.errors?.['required']">Email is required.</div>
<div *ngIf="registrationForm.get('email')?.errors?.['email']">Invalid email format.</div>
</div>
</div>
<div>
<label>
Address:
<textarea id="address" formControlName="address" placeholder="Your address"></textarea>
</label>
<div *ngIf="registrationForm.get('address')?.invalid && (registrationForm.get('address')?.dirty || registrationForm.get('address')?.touched)">
<div *ngIf="registrationForm.get('address')?.errors?.['required']">Please provide your address.</div>
<div *ngIf="registrationForm.get('address')?.errors?.['minlength']">Address must be at least 30 characters long.</div>
</div>
</div>
<div>
<label>
Country:
<input id="country" formControlName="country" placeholder="Your country">
</label>
<div *ngIf="registrationForm.get('country')!.invalid && (registrationForm.get('country')!.dirty || registrationForm.get('country')!.touched)">
<div *ngIf="registrationForm.get('country')?.errors?.['required']">Please provide your country name.</div>
<div *ngIf="registrationForm.get('country')?.errors?.['minlength']">Please provide country name with at least two characters.</div>
</div>
</div>
<div>
<label>
Postal Code:
<input id="pin" formControlName="pin" placeholder="Your pin code">
</label>
<div *ngIf="registrationForm.get('pin')?.invalid && (registrationForm.get('pin')?.dirty || registrationForm.get('pin')?.touched)">
<div *ngIf="registrationForm.get('pin')?.errors?.['required']">Please provide your pin/zip code.</div>
<div *ngIf="registrationForm.get('pin')?.errors?.['minlength']">Please provide postal code with at least 5 digits.</div>
</div>
</div>
<button type="submit" [disabled]="registrationForm.invalid">Register</button>
</form>
<router-outlet />
The template uses *ngIf and registrationForm.get('controlName')?.errors to display specific validation messages based on the errors returned by the validators.
registrationForm.get('controlName')?.touched or registrationForm.get('controlName')?.dirty ensures messages appear only after interaction.
[disabled]="registrationForm.invalid" disables the submit button until the entire form is valid.
This example shows how to work with synchronous validators in reactive forms of Angular framework.
Asynchronous Validators
Asynchronous validators, which involve operations like server requests and return Promises or Observables. Basically, asynchronous validators are used to perform validation that requires an asynchronous operation, such as making an HTTP request to a backend API to check for unique data (e.g., username or email availability) or validating against external services.
Key characteristics of asynchronous validators are:
- Asynchronous validators return an
Observableor aPromise. - If the input is valid, the
ObservableorPromiseshould eventually resolve to null. - If the input is invalid, it should resolve to a
ValidationErrorsobject, which is typically an object with key-value pairs describing the validation error (e.g.,{ 'usernameExists': true }). - If using an
Observable, it must complete for the form to use the last emitted value for validation.
The diagram for the asynchronous validation is given below:
Service Class
The service class here will simulate the HTTP call for server side checking for the existing email address.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { delay, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class EmailService {
constructor(private http: HttpClient) {}
checkEmailExists(email: string): Observable<boolean> {
// Simulate an API call with a delay
const existingEmails = ['test@roytuts.com', 'testemail@roytuts.com'];
return of(existingEmails.includes(email.toLowerCase())).pipe(
delay(500) // Simulate network latency
);
}
}
Component Class
The component class is updated with the relevant logic for checking the asynchronous validation.
import { Component, signal, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
//import { delay } from 'rxjs/operators';
import { map, catchError } from 'rxjs/operators';
import { EmailService } from './email.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
standalone: false,
styleUrl: './app.css'
})
export class App implements OnInit {
protected readonly title = signal('Angular Reactive Forms');
registrationForm!: FormGroup;
constructor(private fb: FormBuilder, private emailService: EmailService) {}
ngOnInit() {
this.registrationForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email], [this.emailExistsValidator()]],
address: ['', [Validators.required, Validators.minLength(30)]],
country: ['', [Validators.required, Validators.minLength(2)]],
pin: ['', [Validators.required, Validators.minLength(5), Validators.pattern(/^\d{6}$/)]]
});
}
emailExistsValidator() {
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
const email = control.value;
if (!email) {
return of(null); // No value, no validation needed
}
return this.emailService.checkEmailExists(control.value).pipe(
map(exists => (exists ? { emailTaken: true } : null)),
catchError(() => of(null)) // Handle API errors gracefully
);
};
}
onSubmit() {
if (this.registrationForm.valid) {
console.log('Form Submitted!', this.registrationForm.value);
} else {
console.log('Form is invalid.');
}
}
}
Asynchronous validators are passed as the third argument to FormControl (or FormBuilder.control). These return a Promise or Observable that eventually resolves to null (valid) or an error object (invalid). They are typically used for server-side validation.
Template Form
The required update is done on the template form.
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
<div>
<label>
Full Name:
<input id="name" formControlName="name" placeholder="Full name">
</label>
<div *ngIf="registrationForm.get('name')?.invalid && (registrationForm.get('name')?.dirty || registrationForm.get('name')?.touched)">
<div *ngIf="registrationForm.get('name')?.errors?.['required']">Please provide a full name.</div>
<div *ngIf="registrationForm.get('name')?.errors?.['minlength']">Name must be at least 3 characters.</div>
</div>
</div>
<div>
<label>
Email:
<input id="email" formControlName="email" placeholder="Your email">
</label>
<div *ngIf="registrationForm.get('email')?.pending">Checking email...</div>
<div *ngIf="registrationForm.get('email')?.invalid && (registrationForm.get('email')?.dirty || registrationForm.get('email')?.touched)">
<div *ngIf="registrationForm.get('email')?.errors?.['required']">Email is required.</div>
<div *ngIf="registrationForm.get('email')?.errors?.['email']">Invalid email format.</div>
<div *ngIf="registrationForm.get('email')?.errors?.['emailTaken']">Email is already taken.</div>
</div>
</div>
<div>
<label>
Address:
<textarea id="address" formControlName="address" placeholder="Your address"></textarea>
</label>
<div *ngIf="registrationForm.get('address')?.invalid && (registrationForm.get('address')?.dirty || registrationForm.get('address')?.touched)">
<div *ngIf="registrationForm.get('address')?.errors?.['required']">Please provide your address.</div>
<div *ngIf="registrationForm.get('address')?.errors?.['minlength']">Address must be at least 30 characters long.</div>
</div>
</div>
<div>
<label>
Country:
<input id="country" formControlName="country" placeholder="Your country">
</label>
<div *ngIf="registrationForm.get('country')!.invalid && (registrationForm.get('country')!.dirty || registrationForm.get('country')!.touched)">
<div *ngIf="registrationForm.get('country')?.errors?.['required']">Please provide your country name.</div>
<div *ngIf="registrationForm.get('country')?.errors?.['minlength']">Please provide country name with at least two characters.</div>
</div>
</div>
<div>
<label>
Postal Code:
<input id="pin" formControlName="pin" placeholder="Your pin code">
</label>
<div *ngIf="registrationForm.get('pin')?.invalid && (registrationForm.get('pin')?.dirty || registrationForm.get('pin')?.touched)">
<div *ngIf="registrationForm.get('pin')?.errors?.['required']">Please provide your pin/zip code.</div>
<div *ngIf="registrationForm.get('pin')?.errors?.['minlength']">Please provide postal code with at least 5 digits.</div>
</div>
</div>
<button type="submit" [disabled]="registrationForm.invalid">Register</button>
</form>
<router-outlet />
When you type the email address you will see different messages as you type: Invalid Email format followed by Checking email… (while it checks for the existing email address), etc.
You will find similar Angular reactive form as given in the below image:



No comments