Form Validation with Angular Reactive Forms

Form Validation with Angular Reactive Forms

Managing client side validation with Angular's built-in Reactive Forms Module

In this article we will delve into form validation techniques using Angular's reactive forms, including built-in validators, custom validators, and asynchronous validators.

For an introduction into Reactive Forms, check out The Complete Guide to Getting Started with Angular Reactive Forms.

Implementing Basic Validation

Angular Reactive Forms provide a simple way to implement complex form validations in web applications. By using validators, we can make sure the data users enter meets certain criteria. In this guide, we'll explore how to implement basic validation using both synchronous and asynchronous built-in and custom validation functions.

Introduction to Validators

Validators in Angular are functions that assess the validity of form controls or form groups based on certain criteria. Angular provides a set of built-in validators to perform common validation tasks, such as checking for required fields, minimum and maximum lengths, and pattern matching.

Applying Built-in Validators

Reactive Forms include a variety of built-in validators that can be easily applied to form controls.

  • Required: Ensures that a field is not empty.

  • minLength: Specifies the minimum length required for a field.

  • maxLength: Specifies the maximum length allowed for a field.

  • pattern: Validates input against a regular expression pattern.

Custom Validators

Built-in validators cover many common scenarios, but there are instances where custom validation logic is necessary. We can create custom validators tailored to our specific requirements.

Creating a Custom Validator Function

To create a custom validator in Angular, you need to define a function that accepts a form control as input and returns either null if the control is valid or an error object if it's invalid. It's not important where this function is defined, and can be standalone or part of a service.

import { AbstractControl } from '@angular/forms';

// Note that this return type conforms to ValidatorFn in @angular/forms
export function customValidator(control: AbstractControl): { [key: string]: any } | null {
  // Custom validation logic
  if (control.value && !isValid(control.value)) {
    return { 'customError': true };
  }
  return null;
}

The presence of a non-empty returned object in the validator lets Angular know that the validator returned an error and sets the control's validation state accordingly. From the component's template, we can check if the control has an error called customError and, if so, display a message. More about this in a later section dedicated to displaying validation messages.

Applying the Custom Validator to Form Controls

Once you've defined the custom validator function, apply it to form controls by passing it as the second argument of the FormControl constructor, which is an array of synchronous validators.

import { FormControl, FormGroup, Validators } from '@angular/forms';

// Creating a form group with custom validation
const myForm = new FormGroup({
  'customField': new FormControl('', [Validators.required, customValidator])
});

Now, changes to the customField form control will be validated against the validators specified above on each field change and it's valid property will be updated to reflect the result.

Composing Validators

Alternatively, we could use the Validators.compose function above to define the validator. The primary reason to use Validators.compose() is for reusing several validators. For example, if you need to ensure a value is between 0 and 100, you would initially write:

const myForm = new FormGroup({
  'customField': new FormControl('', [Validators.min(0), Validators.max(100)])
});

However, if you need to apply this validation in multiple places within your app, you can avoid repeating code by creating your own validator. You do this by composing the existing validators into a single validator, making it available, and then reusing it wherever needed:

// custom-validators.ts
import { Validators } from '@angular/forms';

export class CustomValidators {
  readonly betweenZeroHundred = Validators.compose([
    Validators.min(0),
    Validators.max(100),
  ]);
}

// wherever you want to define your form
const myForm = new FormGroup({
  'customField': new FormControl('', [CustomValidators.betweenZeroHundred()])
});

Advanced Validation Techniques

Let's explore advanced validation techniques, including cross-field validation and asynchronous validators.

Cross-field Validation

Cross-field validation involves validating the relationship between multiple form fields. This technique is useful when the validity of one field depends on the value of another. For instance, you might want to ensure that the value of one field is greater than or equal to another.

Creating a Validator for Comparing Two Form Fields

To create a cross-field validator in Angular, you can define a custom validator function that evaluates the values of multiple form controls and returns an error if the validation criteria are not met.

import { FormGroup, ValidatorFn, ValidationErrors } from '@angular/forms';

export const passwordMatchValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
  const password = control.get('password');
  const confirmPassword = control.get('confirmPassword');

  if (password && confirmPassword && password.value !== confirmPassword.value) {
    return { 'passwordMismatch': true };
  }

  return null;
};

In this example, passwordMatchValidator is a validator function that takes a form group as input and checks if the values of the password and confirmPassword fields match. If they don't match, it returns an error object with the key passwordMismatch.

Once you've defined the validator function, you can add it to your form group:

this.myForm = new FormGroup(
  {
    password: new FormControl('', Validators.required),
    confirmPassword: new FormControl('', Validators.required),
  },
  { validators: this.passwordMatchValidator }
);

Async Validators

Async validators are used when validation requires interaction with external services or asynchronous operations, such as fetching data from a server.

Implementing an Asynchronous Validator

To implement an asynchronous validator, you define a function that returns a promise or an observable of <ValidationErrors | null>, where ValidationErrors is simply defined as { [key: string]: any }.

Angular defines an asynchronous validator as:

export declare interface AsyncValidatorFn {
    (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
}

The function exampleAsyncValidator in the example that follows is a factory function that creates and returns an asynchronous validator function conforming to Angular's AsyncValidatorFn interface.

This function is then passed as an argument to the AsyncValidatorFn when creating form controls.

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {
  FormControl,
  Validators,
  ValidationErrors,
  AsyncValidatorFn,
  ReactiveFormsModule,
  AbstractControl,
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

// Async Validator Function
function exampleAsyncValidator(): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    const isInvalid = control.value === 'invalid';
    // Simulate async operation with delay
    return of(isInvalid ? { invalidAsync: true } : null).pipe(delay(1000));
  };
}

@Component({
  selector: 'app-example-async-validator',
  template: `
    <div>
      <form>
        <label>
          Enter value (type "invalid" to see async validation):
          <input type="text" [formControl]="inputControl" />
        </label>
        <div *ngIf="inputControl.hasError('required')">
          This field is required.
        </div>
        <div *ngIf="inputControl.hasError('invalidAsync')">
          The value "invalid" is not allowed.
        </div>
      </form>
    </div>
  `,
  standalone: true,
  styles: [],
  imports: [ReactiveFormsModule, CommonModule],
})
export class ExampleAsyncValidatorComponent {
  // FormControl with async validator
  inputControl = new FormControl('', {
    validators: [Validators.required], // synchronous validators
    asyncValidators: [exampleAsyncValidator()], // async validators
    updateOn: 'blur', // triggers validation on blur. can also use 'change' and 'submit'
  });

  constructor() {}
}

Use Cases for Async Validators

Async validators are useful for scenarios where validation depends on external factors, such as

  • Checking the availability of a username during registration

  • Verifying the uniqueness of an email address

  • Validating inputs against a database

Displaying Validation Messages

We can access the form control (or group) errors property to display validation errors in our template.

Creating Dynamic Error Messages Based on Validation Error

Reactive Forms allow for dynamic generation of error messages based on the type of validation error encountered. This can be achieved by accessing the errors property of a form control, which contains information about the validation errors. We can then map these errors to corresponding error messages in our templates.

<div *ngIf="form.get('email').errors?.required">
  Email is required.
</div>

<!-- Assuming that our custom validator was applied to the email control -->
<div *ngIf="form.get('email').errors?.customError">
  Our custom error from a previous example failed.
</div>

<!-- Display validation message if passwords don't match -->
<div *ngIf="myForm.hasError('passwordMismatch') && (myForm.get('confirmPassword').dirty || myForm.get('confirmPassword').touched)">
   Passwords do not match.
</div>
💡
Note that in your template you may receive an error Property 'required' comes from an index signature, so it must be accessed with ['required']. In this case, you can either use the index accessor like ngIf="inputControl.errors && inputControl.errors['required']" or use the hasError function like ngIf="inputControl.hasError('required')"

Displaying Loading Indicators During Async Validation

Indicating that an async validator is loading in a template involves checking the status of the form control being validated. Form controls have a status property that can be in one of several states: VALID, INVALID, PENDING, or DISABLED. When an async validator is running, the control's status is set to PENDING.

We can use this PENDING status in the template to display a loading indicator to our user. For example, if our username form control had an asynchronous validator that checked uniqueness of a username, we could update the UI to indicate that the validator was running like:

<div *ngIf="myFormGroup.get('username')?.status === 'PENDING'">
  Checking availability...
</div>

Styling Invalid Input Fields

Reactive Forms provide CSS classes that can be dynamically applied to form controls to provide your users with visual feedback. These controls are added by the framework and can be referenced from your styles.

.ng-invalid {
  border-color: #f00; /* Change border color for invalid fields */
  background-color: #ffe6e6; /* Light red background for invalid fields */
}

.ng-valid {
  border-color: #0f0; /* Change border color for valid fields */
  background-color: #e6ffe6; /* Light green background for valid fields */
}

For a full list of CSS classes that Angular attaches to see Control status CSS classes