fbpx Skip to content

Aquent | DEV6

Angular Custom Cross Field Validators in Reactive Forms

Written by: Gagan Kaur

Today we will learn how to create custom cross field validators in Angular Reactive Forms. Let’s say, the user form which you are building for your project has some tricky validation which is not covered by Angular Built-in validators — you can create your own fancy custom validator.

For a full list of available Built-in validators please visit this URL: https://angular.io/api/forms/Validators

Alright back to custom validator creation. Below is a user form which we use to explain the concept. Currently, the form controls firstName, lastName and email do not have any validations attached to them.

ngOnInit() {
    this.userForm = this.formBuilder.group({
        firstName: [‘’],
        lastName: [‘’],
        email: [‘’],
    });
}

To add validators, first import them into the component class:

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

Now, we can add some of the built-in validators to user form group.

ngOnInit() {
    this.userForm = this.formBuilder.group({
        firstName: [‘’,[Validators.required, Validators.maxLength(25)]],
        lastName: [‘’,[Validators.required, Validators.maxLength(20)]],
        email: [‘’,[Validators.required, Validators.email]],
    });
}

I have a use case where I limit the combined first name and last name length to less than 40 characters. To accomplish this, I have to write a custom validator which takes the value of both the form controls. In order to do so, I have to add validation to the parent formGroup.

ngOnInit() {
    this.userForm = this.formBuilder.group({
        firstName: [‘’,[Validators.required, Validators.maxLength(25)]],
        lastName: [‘’,[Validators.required, Validators.maxLength(20)]],
        email: [‘’,[Validators.required, Validators.email]],
    },
	{validator: combinedName(‘firstName’,’lastName’,40)
	});
}

All that is left to do so is to implement the combinedName Validator. First, we implement the ValidatorFn interface.

Interface ValidatorFn {
	(c:AbstractControl): ValidationErrors | null
}

In our case, instead of FormControl we receive the entire FormGroup. The rest is pretty straightforward.

export function combinedName(firstName: string, lastName: string, maxlength: number) {
   return (group: FormGroup): {[key: string]: any} => {
      let firstNameControl = group.controls[firstName];
      let lastNameControl = group.controls[lastName];
      let combinedName: string = firstNameControl.value + lastNameControl.value;
      if (IsEmptyValidator.isEmpty(firstNameControl.value) || IsEmptyValidator.isEmpty(lastNameControl.value) || combinedName.length >= maxlength) {
         return {
            'combinedName': true
         };
      }
   }
}

Let’s go through above code snippet. There are 3 parameters in the function declaration. FirstName and lastName strings are passed in to get the associated formControl value from FormGroup. Maxlength is passed in for checking the length. After making the necessary null checks and calculation of the combined name length, if the check fails, it returns an error object with the name of error as ‘combinedName’.

Now, let’s go through the template part of the reactive form where we catch these errors and display suitable error messages to user.

<ng-template>
<div class="col-4 name">
 <div>{{ 'FIRST_NAME' | translate | uppercase }}</div>
 <input type="text" formControlName="firstName" [readOnly]="!isEditing" [ngClass]="{'is-invalid':form.get('firstName').errors || form.errors?.combinedName}" class="form-control form-control-lg" [class.form-control-plaintext]="!isEditing">
  <div *ngIf="form.get('firstName').errors" class="">
   <div *ngIf="form.get('firstName').hasError('required')">{{'FIRST_NAME_REQUIRED' |   translate}}</div>
   <div *ngIf="form.get('firstName').hasError('maxlength')">{{'FIRST_NAME_MAXLENGTH' | translate}}</div>
  </div>
  <div *ngIf="form.errors?.combinedName" class="">
    {{'VALIDATION_ERROR_COMBINED_NAME_LENGTH' | translate}}
   </div>
</div>

Look closely at the pattern to see how we check for errors. There is a trivial difference in where the errors are stored for individual form controls and form groups. So, for formControl it’s stored in the formControl itself and you can check by calling form.get(‘firstName’).errors.

And, for formGroup you can check by querying form.errors.

One last method to cover is the Validators.compose. This method comes in handy if you had to add more than one custom validator to your formGroup.

ngOnInit() {
    this.userForm = this.formBuilder.group({
        firstName: [‘’,[Validators.required, Validators.maxLength(25)]],
        lastName: [‘’,[Validators.required, Validators.maxLength(20)]],
        email: [‘’,[Validators.required, Validators.email]],
    },
	{validator: Validators.compose([combinedName(‘firstName’,’lastName’,40),
	   customEmail])
	});
}

In above snippet we added two custom validators to formGroup.

For further read on other types of validations in Angular please check our blog on Async Validation in Angular: https://www.dev6.com/news/creating-an-angular-asyncvalidator-in-a-reactive-form/