User interface
Angular HTML SPA Typescript Visual Studio Code

How to Implement Cross-Field Validations in an Angular Form

Welcome to today’s post.

In today’s post I will be discussing how to use cross-field validations in an Angular Reactive Form.

In a previous post I showed how to use built-in and custom validators within an Angular Reactive Form.

What I didn’t show was how to validate multiple fields within the same form group. Validation of multiple fields within the same validation is known as cross-field validation.

Before I show how to identify candidates for cross-field validation and how the validations are implemented, I will show how the FormBuilder and FormGroup objects are initialized in the form component source in the next section.

FormBuilder and FormGroup Initializations

Before we can implement a cross-field validation, we will need to construct our FormBuilder form group as shown starting with the following namespaces:

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

We require an instance of our form group that will hold the form controls for our reactive form:

meetingForm: any;

An instance of our FormBuilder class is injected into our component as shown and passed into our form initialization method as shown:

constructor(private fb: FormBuilder) { 
    this.initForm(fb);
}

Next, our form initialization method is used to initialize the form group and form controls as shown:

this.meetingForm = fb.group({
    library: ['', Validators.required],
    room: ['', Validators.required],
    meetingDate: [currDateString, Validators.required],
    timeStart: [this.timeStart, Validators.required],
    timeEnd: [this.timeEnd, Validators.required]
});

The above code will give us a Reactive Form with the basic built-in required validation but without any custom validations.

Identifying Fields for Cross-Field Validations

In this section I will show how to identify fields that can be used for cross-field validations.

Below is the Reactive form using Material and Bootstrap themes for its controls without any custom validations:

Looking at the above interface controls, we can see that the start time and end time are a range that consists of a finite selection of discrete minutes between a starting time and an ending time.  

Given the above rationale, the most obvious candidates for the cross-field validation are the pair of time (hour and minute) fields:

  • Start Time
  • End Time

The HTML template script for the time fields is shown below:

<div class="form-group">
<label for="timeStart">Start Time:</label>
    <ngb-timepicker  
        [meridian]="meridianStart"
        placeholder="Start Time" 
        placement="right" 
        ngbTooltip="Start Time" 
        formControlName="timeStart">
    </ngb-timepicker>    
</div>
      
<div *ngIf="timeStart.invalid && 
    (timeStart.dirty || timeStart.touched || submitted)"
        class="alert alert-danger">
    <div *ngIf="timeStart.errors.required">
        Time Start is required.
    </div>
</div>

<div class="form-group">
    <label for="timeEnd">End Time:</label>

    <ngb-timepicker  
        [meridian]="meridianEnd"
        placeholder="End Time" 
        placement="right" 
        ngbTooltip="End Time" 
        formControlName="timeEnd">
    </ngb-timepicker>
</div>
  
<div *ngIf="timeEnd.invalid && 
    (timeEnd.dirty || timeEnd.touched || submitted)"
    class="alert alert-danger">
    <div *ngIf="timeEnd.errors.required">
        Time End is required.
    </div>
</div>

Each time field has a built-in required validation which is tested with the invalid property of the Angular Forms Validators class.

A Custom Cross-Field Validation Function

Implementation of a cross-field validation method is quite straightforward and requires us to implement a basic function that is exported within our application.

Below is an implementation of a custom validation that validates the rule that the start time is before the end time.

import { AbstractControl, ValidationErrors, ValidatorFn } 
from "@angular/forms";

export const timeStartAndEndValidator: ValidatorFn = 
(control: AbstractControl): ValidationErrors | null => 
{
    const startTime = control.get('timeStart');
    const endTime = control.get('timeEnd');

    const isTimeDurationOrdered = startTime && endTime &&   
        (startTime.value.hour*60 + startTime.value.minute <=  
         endTime.value.hour*60 + endTime.value.minute);
    
    return startTime && endTime && !isTimeDurationOrdered ? { 
        timeStartAndEndValidator: true } : null;
};

The calculation involves computing the total minutes of each time and comparing them.

When the result of the validation evaluates to true, we return null from the validation function. 

When the result of the validation evaluates to false, we return the following JSON object from the validation function:

{ timeStartAndEndValidator: true }

Note also that the AbstractControl parameter for our validator function is of type FormGroup as shown below in the debugger:

To determine the keys and values of each of our time field controls, we expand each of these controls to view the JSON structure as shown:

The JSON structure we used to compute the value of each time control is:

{
    hour: number,
    minute: number,
    second: number
} 

Next, to integrate the above validation function into our reactive form, we apply the following steps:

First, we import the custom validation function:

import { timeStartAndEndValidator } 
from 'src/app/utilities/time-interval.validator';

Next, we append a reference to the custom validation function to the end of the form group object as shown:

this.meetingForm = fb.group(
  {
    library: ['', Validators.required],
    room: ['', Validators.required],
    meetingDate: [currDateString, Validators.required],
    timeStart: [this.timeStart, Validators.required],
    timeEnd: [this.timeEnd, Validators.required]
  }, 
  { 
    validators: timeStartAndEndValidator 
  }
);

To allow our HTML template to apply the custom cross-field validation whenever one of the time fields is changed, we have the following conditional DIV HTML markup: 

<div *ngIf="meetingForm.errors && 
    meetingForm.errors['timeStartAndEndValidator'] && 
    (meetingForm.touched || meetingForm.dirty)" 
    class="cross-validation-error-message alert alert-danger">
        The Start Time must be earlier than the End Time.
</div>

Notice that in the above condition, we ensure that the following conditions are satisfied:

  1. The errors array within the form control object is initialized.
  2. The value of the timeStartAndEndValidator validation property is initialized.
  3. The form has been touched or modified (is dirty).

The errors array is only initialized when at least one of the form controls fails one of the built-in or custom validations. When all form controls pass all the built-in and custom validations, the errors array is initialized to null.

Below is a screen shot of a successful validation, where all fields satisfy validations:

Below is a screenshot of when the time controls fail the custom validation when the time value ordering of the start time is later than the end time:

In the above error state, the errors array object is assigned to an object returned from the validation function:

You have seen from the above how straightforward it is to implement custom cross-field validations for a Reactive Form within an Angular application.

With the knowledge of how to implement a custom single field validation and built-in validations we have a powerful toolkit to be able to implement some quite complex user interface validations.

An introduction to Angular forms validation can be found on the Angular site.

That is all for today’s post.

I hope you have found the above post both useful and informative.

Social media & sharing icons powered by UltimatelySocial