Tutorial

How To Use Custom Form Validation in Angular

Angular

Introduction

Validators are used to ensure that the values in a form meet certain requirements. They are available to Template-Driven Forms or Reactive Forms in Angular applications.

There are several built-in validators like required, email, pattern, and minLength. It is also possible to develop custom validators to address functionality that is not handled by a built-in validator.

For example, a phone number validator would consist of an input field and will not be considered valid unless the value is ten digits long.

Here is a screenshot of a phone number input field that is providing an invalid number that is nine digits long:

Screenshot of an input field with an invalid phone number. The input is highlighted in a red border and there is an error message indicating the field expects phone number values to be 10 digits long.

And here is a screenshot of a phone number input field that is providing a valid number that is ten digits long:

Screenshot: of an input field with a valid phone number. The input is highlighted with a blue border and there is no error message.

In this tutorial, you will construct a custom validator for a phone number input field in an Angular application.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v15.2.1, npm v6.14.8, @angular/core v11.0.0, and @angular/forms v11.0.0.

Setting Up the Project

For the purpose of this tutorial, you will build from a default Angular project generated with @angular/cli.

  • npx @angular/cli new angular-custom-validation-example --style=css --routing=false --skip-tests

Note: Alternatively, you can globally install @angular/cli.

This will configure a new Angular project with styles set to “CSS” (as opposed to “Sass”, Less", or “Stylus”), no routing, and skipping tests.

Navigate to the newly created project directory:

  • cd angular-custom-validation-example

At this point, you will have a new Angular project.

Using Validator in a Template-Driven Form

Directives are used for validation in template-driven forms. For this example, you will create a phone-number-validator directive with @angular/cli.

First, open your terminal and use the @angular/cli package that was installed as a dev dependency to generate a new directive:

  • ./node_modules/@angular/cli/bin/ng generate directive phone-number-validator

This will create phone-number-validator.directive.ts and phone-number-validator.directive.spec.ts. It will also add PhoneNumberValidatorDirective to app.module.ts.

Next, open phone-number-validator.directive.ts in your code editor. Add Validator, AbstractControl, and NG_VALIDATORS:

src/app/phone-number-validator.directive.ts
import { Directive } from '@angular/core';
import { AbstractControl, Validator, NG_VALIDATORS } from '@angular/forms';

@Directive({
  selector: '[appPhoneNumberValidator]',
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: PhoneNumberValidatorDirective,
    multi: true
  }]
})
export class PhoneNumberValidatorDirective implements Validator {
  validate(control: AbstractControl) : {[key: string]: any} | null {
    if (control.value && control.value.length != 10) {
      return { 'phoneNumberInvalid': true };
    }
    return null;
  }
}

This code creates a directive which implements Validator of @angular/forms. It will need the following implementation method: validate(control: AbstractControl): : {[key: string]: any} | null. This validator will return an object - { 'phoneNumberInvalid': true } - if the value fails the condition of not being equal to a length of ten characters. Otherwise, if the value passes the condition, it will return null.

Next, open your terminal and use the @angular/cli package that was installed as a dev dependency to generate a new directive:

  • ./node_modules/@angular/cli/bin/ng generate component template-driven-form-example --flat

This command will create template-driven-form-example.component.ts and template-driven-form-example.component.html files. It will also add TemplateDrivenFormExampleComponent to app.module.ts.

Next, open template-driven-form-example.component.ts in your code editor and add phone with an initial value of an empty string:

src/app/template-driven-form-example.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-template-driven-form-example',
  templateUrl: './template-driven-form-example.component.html',
  styleUrls: ['./template-driven-form-example.component.css']
})
export class TemplateDrivenFormExampleComponent {
  phone = '';
}

Angular adds the return value of the validation function in the errors property of FormControl / NgModel. If the errors property of the FormControl / NgModel is not empty then the form is invalid. If the errors property is empty then the form is valid.

To use the directive in a template-driven form, open template-driven-form-example.component.html and add the following code:

src/app/template-driven-form-example.component.html
<div class="form-group">
  <label>Phone
    <input
      type="text"
      class="form-control"
      name="phone"
      [(ngModel)]="phone"
      #phoneNgModel="ngModel"
      appPhoneNumberValidator
      [class.is-invalid]="(phoneNgModel.touched || phoneNgModel.dirty) && phoneNgModel.errors?.phoneNumberInvalid"
    >
  </label>
  <span
    class="invalid-feedback"
    *ngIf="(phoneNgModel.touched || phoneNgModel.dirty) && phoneNgModel.errors?.phoneNumberInvalid"
    > 
      Phone number must be 10 digits
  </span>
</div>

This code creates an <input> element and <span> with an error message. The <input> element uses the ngModel and the appPhoneNumberValidator selector for the directive.

If the <input> has been touched or dirty and the validation is not passing, two things will occur. First, the class is-invalid will be applied to the <input>. Second, the <span> with the error message will display.

Note: Some of the classes here - form-group, form-control, invalid-feedback, and is-valid - are part of the Bootstrap Framework. These are not necessary to complete this tutorial but can provide visual aesthetics to the form.

Then, open app.module.ts in your code editor and add FormModule:

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { PhoneNumberValidatorDirective } from './phone-number-validator.directive';
import { TemplateDrivenFormExampleComponent } from './template-driven-form-example.component';

@NgModule({
  declarations: [
    AppComponent
    PhoneNumberValidatorDirective,
    TemplateDrivenFormExampleComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Finally, open app.component.html and replace the content with your TemplateDrivenFormExample:

src/app/app.component.html
<app-template-driven-form-example></app-template-driven-form-example>

You can run the npm start command and interact with your input in a web browser. If you enter less than or more than 10 characters to the phone field, it will display an error message.

At this point, you have a custom validator using a directive in a template-driven form.

Using Validator in a Reactive Forms

Instead of directives, Reactive Forms use functions for validation.

First, open your terminal and use the @angular/cli package that was installed as a dev dependency to generate a new directive:

  • ./node_modules/@angular/cli/bin/ng generate component reactive-form-example --flat

This command will create reactive-form-example.component.ts and reactive-form-example.component.html files. It will also add ReactiveFormExampleComponent to app.module.ts.

Next, open reactive-form-example.component.ts in your code editor and add FormBuilder and AbstractControl:

src/app/reactive-form-example.component.ts
import { Component, OnInit } from "@angular/core";
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-reactive-form-example',
  templateUrl: './reactive-form-example.component.html',
  styleUrls: ['./reactive-form-example.component.css']
})
export class ReactiveFormExampleComponent implements OnInit {
  myForm: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.myForm = this.fb.group({
      phone: ['', [ValidatePhone]]
    });
  }

  saveForm(form: FormGroup) {
    console.log('Valid?', form.valid); // true or false
    console.log('Phone Number', form.value.phone);
  }
}

function ValidatePhone(control: AbstractControl): {[key: string]: any} | null  {
  if (control.value && control.value.length != 10) {
    return { 'phoneNumberInvalid': true };
  }
  return null;
}

This code creates a ValidatePhone function and adds it to the validators array of FormControl.

Open reactive-form-example.component.html in your code editor and create the following form:

src/app/reactive-form-example.component.html
<form
  class="needs-validation"
  novalidate
  [formGroup]="myForm"
  (ngSubmit)="saveForm(myForm)"
>
  <div class="row">
    <div class="form-group col-sm-4">
      <label>
        Phone
        <input
          type="text"
          class="form-control"
          formControlName="phone"
          [class.is-invalid]="(myForm.get('phone').touched || myForm.get('phone').dirty) && myForm.get('phone').invalid"
        >
      </label>
      <span
        class="invalid-feedback"
        *ngIf="(myForm.get('phone').touched || myForm.get('phone').dirty) && myForm.get('phone').invalid"
      >
        Phone number must be 10 digits
      </span> 
    </div>
  </div> 
</form>

Unlike the template-driven form, this form features a form and uses [formGroup], (ngSubmit), formControlName, and get.

Then, open app.module.ts in your code editor and add ReactiveFormsModule:

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { PhoneNumberValidatorDirective } from './phone-number-validator.directive';
import { ReactiveFormExampleComponent } from './reactive-form-example.component';
import { TemplateDrivenFormExampleComponent } from './template-driven-form-example.component';

@NgModule({
  declarations: [
    AppComponent,
    PhoneNumberValidatorDirective,
    ReactiveFormExampleComponent,
    TemplateDrivenFormExampleComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Finally, open app.component.html and replace the content with your ReactiveFormExample:

src/app/app.component.html
<app-reactive-form-example></app-reactive-form-example>

You can run the npm start command and interact with your input in a web browser. If you enter less than or more than 10 characters to the phone field, it will display an error message.

At this point, you have a custom validator using a function in a reactive form.

Conclusion

In this article, you were introduced to adding custom validation for template-driven forms and reactive forms in an Angular application.

Custom validation allows you to ensure values provided by users fit within your expectations.

For a deeper understanding of the concepts from this post, visit this post on Providers and read about AbstractControl.

Creative Commons License