Angular Form Validation

Angular: The Full Gamut Edition

Charlie Greenman
September 06, 2020
12 min read
razroo image

The beauty of Angular Form Validation

One of main reasons why one would choose to use the internal Angular Form Group architecture, is the way it eases validation. Angular has a series of built in validators, that can be used for the fields in your form. For the sake of clarity, and brevity, I would like to jot down the name of current Angular Validators here, without going into detail:

  1. min
  2. max
  3. required
  4. requiredTrue
  5. email
  6. minlength
  7. maxlength
  8. pattern
  9. nullValidator
  10. compose
  11. composeAsync

I think for the most part, the above validators speak for themselves as to what they do, with the exception of compose. compose will allow you to combine multiple validators, and return an error map for them. As you can see, the more popular errors, such as min, and max(for use with passwords/usernames) and emails, and patterns are what Angular’s internal validators are there for. There is the ability to make custom validators, and most likely any app you work on, us going to need it’s own custom validators, but before we go ahead and do that, let’s see how we can integrate this with out application.


Integrating Form Validators within Component

Let’s imagine that we have a newsletter component within our application. We want to ensure the user uses an email pattern, and it is also required. We write the following:

ngOnInit () {
  this.newsletterForm = this.fb.group({
    email: ['', [Validators.required , Validators.email]], 
  });
  get email() {
    return this.newsletterForm.get('email');
  } 
}

As we can see using the above we have added two native angular validators to our formControl field email. This is the process wherein we would add Angular form validators to our application. It should be noted that in Angular, when we want to use multiple validators, we place them in a sub-array, within the array for the fb.group.


Integrating Form Validators within HTML Template

If we would like to integrate our form validators within our app, so that when we click on the button, they get triggered, we would do the following:

<ng-container *ngIf="email.invalid && (email.dirty || email.touched)"> 
  <mat-error *ngIf="email.errors.required">Name is required.</mat-error> 
  <mat-error *ngIf="email.errors.email">Email is invalid</mat-error></ng-container>

In the above, we are creating a way of displaying the error if it appears. It should only appear if a user has actually touched the email field.


Custom Validators

As mentioned before, odds are your application, will also need it’s own set of custom validators. Custom validators require their own function that returns true, based on value pass through. That function is then be hooked into the validators array. However, Razroo believes it is more scalable if we create a directive that can be used to automatically create validation for our form. In addition, if we create a separate function for the directive, it allows us to re-use the functionality for the directive without directly using the directive.

Creating Custom Directive Validator

Let’s create a custom directive validator for numbers. We are using the NX workspace. In addition, we are using the Razroo recommended folder structure. A custom directive validator will go in the common folder for directives.

Generate The Directive

ng g lib directive --directory=common

Inside of newly created CommonDirectivesModule, we will create a folder:

cd libs/common/directives/src/lib/;

mdkir number;

So now let’s navigate to our number folder:

cd number;

and run the appropriate Angular CLI command for generating a directive, and exporting it within our CommonDirectivesModule:

ng g directive number —export

This will generate the following output inside of the terminal:

CREATE libs/common/directives/src/lib/number/number.directive.spec.ts (224 bytes)
CREATE libs/common/directives/src/lib/number/number.directive.ts (144 bytes)
UPDATE libs/common/directives/src/lib/common-directives.module.ts (493 bytes)

Create The Function for Directive

Inside of the folder for our number directive, let’s also create a number validator file.

touch validator.ts;

touch validator.spec.ts

Inside of our validate.ts file, we will go ahead and create logic for numbers.

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

function isPresent(obj: any): boolean {
  return obj !== undefined && obj !== null;
}

export const number: ValidatorFn = (control: AbstractControl): {[key: string]: boolean} => {
  if (isPresent(Validators.required(control))) return null;

  let v: string = control.value;
  return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(v) ? null : {'number': true};
}; 

Hook In Validator Function to Directive

We will now go ahead and integrate the number function into our directive.

import { Directive, forwardRef } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, Validator } from '@angular/forms';
import { number } from './validator';

const NUMBER_VALIDATOR: any = {
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => NumberDirective),
  multi: true
};

@Directive({
  selector: '[razrooNumber]',
  providers: [NUMBER_VALIDATOR]
})
export class NumberDirective implements Validator {
  validate(c: AbstractControl): {[key: string]: any} {
    return number(c);
  }
}  

We now have the ability to use this as a directive within our application. It should be noted that we are doing two unique things within our directive.

  1. Validator — Our class is implementing Validator which is the same internal function used for validating a formControl.
  2. We are providing a value called NUMBER_VALIDATOR. NUMBER_VALIDATOR will cause the NG_VALIDATOR injectionToken, which is the Angular provided token for custom providers, to use the value of NumberDirective. The internal Angular forwardRef value is there to make sure that it doesn’t error out if no value exists. (More on forwardRef in another chapter.)

Hook In Directive to Component Template

Now all we need to do, is hook the directive into the template for our component.

<mat-form-field [formGroup]="newsletterForm" class="email-field" >
  <input matInput razrooNumber formControlName="email" placeholder="Your E-mail" required>
</mat-form-field>

<ng-container *ngIf="email.invalid && (email.dirty || email.touched)">
  <mat-error *ngIf="email.errors.number">Not a number</mat-error>
</ng-container>

As you can see, all we had to add to our input is the directive for razrooNumber. We can then the appropriate mat-error within our application in order to hook into the respective error created for number. The reason it is number, is that within our number validator, we are returning a boolean for number.

A Word on This Approach

If you were to take a look at the documentation, it offers two approaches.

  1. Template Driven Custom Validators
  2. Reactive Custom Validators

Razroo recommends this approach for using directives, which is a bit contrary to our general suggestion of using Reactive Forms. It is our understanding, that reactive forms are extremely valuable because they group the form as one. Making it easily accessible from the Typescript side of things. However, in this particular aspect, of re-using custom validators, re-using functions as a utility function, we feel makes the app more brittle than directives. Primarily, because directives are more explicit.

That being said, with our approach, we are separating the logic of validation from the actual directive. Moving forward if your team or other teams within your organization would like to use different approaches, they do have the flexibility to do so.


Cross Field Validation

When I first came across the term ”cross field validation”, I was a bit confused. I thought of it as a way for different field’s validation to be dependent on each other. After reading more, I found that ”cross field” validation is exactly that. We are validating our field based on two field’s correlation to each other.

For instance, let’s say we are working on a finance application. We only want the ”Business Loan Amount” input field to show, if yearly gross income minus expenses, exceeds $100,000. We would be able to set up the internal Angular form validators, so that the loan-amount is invalid, if this cross field reference(gross) is not valid.

The one point to keep in mind is that cross-field validation is distinct from single form validation in two regards:

  1. The validator will be used on two, or more form validators. This means, that we need access to the parent level formControl.
  2. The validator will need to be aware of the specific field that it is operating on. Therefore usability will be limited to the app, and most likely will not make sense to be re-usable.

It should be noted, that based on the above, being that cross-field validation tends to be component-specific, contrary to our recommendation above of using a directive for single field validation, we would recommend using a function. However, as teams are wont to do, they are going to want to have the flexibility to choose how they want to integrate with their app. So we still will be following the separate directive and validate function approach.

Sample Validator Logic

For the sake of brevity, we will not go through the steps we did earlier, re proper folder structure, directive generation, and respective function. Just one note that I would like to make. We mentioned earlier, that due to cross-field validators usually being unique to specific business logic, we prefer the use of reactive form validator functions. Therefore, as opposed to prior single field validators going in the common/directives folder, multiple form validators will go in the app-specific folder (e.g. razroo/common/directives).

However, we will go into the validator logic required for multiple formControl values.

Creating the Service

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

@Injectable({
  providedIn: 'root'
})
export class LoanAmountValidatorService {

  constructor() { }

  identityRevealedValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
    const income = formGroup.get('income');
    const expenses = formGroup.get('expenses');

    return income && expenses && income.value - expenses.value > 100000 ? { 'loanAmount': true } : null;
  };
}  

As we can see in the above code, our logic is now tapping into the entire formGroup control. We are:

  1. Targeting every field that we need.
  2. Creating logic, based on those two fields.

Cross form validation logic integration with our directive, is straightforward and very similar to how single form validation works:

import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
import { LoanAmountValidatorService } from './loan-amount-validator.service';

@Directive({
  selector: '[razrooLoanAmount]',
  providers: [{ provide: NG_VALIDATORS, useExisting: LoanAmountDirective, multi: true }]
})
export class LoanAmountDirective implements Validator {
  constructor(private loanAmountValidatorService: LoanAmountValidatorService) {}

  validate(control: AbstractControl): ValidationErrors {
    return this.loanAmountValidatorService.loanAmountValidator(control)
  }
} 

Creating the Directive

Here we are just pulling in the service, and tucking it into our validate function. So, now using our preferred approach of reactive form validators, for cross-field validation, integrating it, would be as simple as this:

constructor(private fb: FormBuilder,
            private loanAmountValidatorService: LoanAmountValidatorService) { }

ngOnInit() {
  this.newsletterForm = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
  }, {validators: this.loanAmountValidatorService.loanAmountValidator});
}

Creating the error

Integrating the error within our app is exactly as we would have done for a singular field.

<ng-container *ngIf="finance.invalid && (finance.dirty || finance. touched)">
  <mat-error *ngIf="finance.errors.loanAmount">Calculator</mat-error> 
</ng-container>

Async Validation

An async validator is a validator that returns an observable(promises work too, but we do not recommend using a promise within an Angular setting). One other important note is that the Observable must be finite i.e. end. So adding something like first to the async directive more than works.

A classic example with async validation is to see if a username is taken already. In order to give the user instant feedback, we can make an HTTP request every time the user types in the input. A validator is a one to one relationship and is not meant to have a lifecycle beyond that of the initial validation. Using the state for something like this would be overkill. However, we do use GraphQL within our application. So, we are going to return an observable, and call first on it.

Integrating Service with Component for Async Validation

The nature of async validators is they need to use the backend to operate. Therefore using a directive is out of the question. It would be too brittle, and a hack to make work. I am not going to discuss the code behind service actually making requests. The reason is, that use cases wherein we would use asynchronous validation, is usually in an enterprise setting, and it’s one team building it for the entire organization. I feel like the use case for something like this so rare, albeit useful when the time arises. I want to discuss it more conceptually, so you know when to use something like this.

So that being said, let’s assume that we have a service that returns a finite observable. It makes a request and determines whether, or not the user email is already taken.

ngOnInit() {
  this.myForm = this.fb.group({
    name: ['', Validators.required],
    email: [
      '',
      [Validators.required, Validators.email],
      ValidateEmailNotTaken.createValidator(this.signupService)
    ]
  });
}

Within our template, we would do the following:

<form [formGroup]="myForm">
<input type="text" formControlName="name">
<input type="email" formControlName="email">

<!-- Other related errors go here-->

<div *ngIf="myForm.get('email').errors && myForm.get('email').errors.emailUnavailable">
  This email is already taken. 
</div>
</form>  

Performance Concerns

All validators are run after every form value change. As in, when an input value is changed, the validator will be run after every letter/number is added. Synchronous validators, which are not dependent on the backend, generally do not suffer from performance issues in this regard. However, when doing something like making an http request after every time a letter is clicked, it can be expensive. There is a general recommended approach of making validators run on blur, or submit instead. So, let’s say in our app, we want the http request to only be made if the user clicks off of the input, we would do something like the following:

ngOnInit() {
  this.myForm = this.fb.group({
    name: ['', Validators.required],
    email: [
      '',
      [Validators.required, Validators.email],
      ValidateEmailNotTaken.createValidator(this.signupService),
      {updateOn: 'blur'}
    ]
  });
}

A Final Note on Form Validators

Form validation is an integral part of any application. The wonderful way about how Angular does things really shine with the way they approach form validation. Form validation is particularly difficult, because due to it’s repetitive nature, and existing outside of component logic. By following the common/directive architecture for single field validation, and the < app>/common/service/validators architecture for cross field validation, your organization and application, will be in a very good place to scale.

Subscribe to the Razroo Angular Newsletter!

Razroo takes pride in it's Angular newsletter, and we really pour heart and soul into it. Pass along your e-mail to recieve it in the mail. Our commitment, is to keep you up to date with the latest in Angular, so you don't have to.

More articles similar to this

footer

Razroo is committed towards contributing to open source. Take the pledge towards open source by tweeting, #itaketherazroopledge to @_Razroo on twitter. One of our associates will get back to you and set you up with an open source project to work on.