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 FormControl instance as an argument and directly return either null (if the value is valid) or a ValidationErrors object (if there are validation errors).
  • built-in validators such as Validators.required, Validators.email, Validators.minLength, and Validators.maxLength, etc.

The diagram for the synchronous validation is given below:

Angular Sync Validation

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 Observable or a Promise.
  • If the input is valid, the Observable or Promise should eventually resolve to null.
  • If the input is invalid, it should resolve to a ValidationErrors object, 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:

Angular Async Validation

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:

Image

Source Code

Download

Share

Related posts

No comments

Leave a comment