Author avatar

Gaurav Singhal

Building Custom Validators in Angular

Gaurav Singhal

  • Jan 13, 2020
  • 14 Min read
  • 8,886 Views
  • Jan 13, 2020
  • 14 Min read
  • 8,886 Views
Languages Frameworks and Tools
Angular

Introduction

Most applications for web, desktop, and mobile contain various forms with fields to collect user input. But before submitting values to the server, we must validate them.

Validating form values before the submit operation is a crucial task because most of the application contains the form, so there is a chance a user may enter incorrect values or a hacker may try to steal our valuable data.

The Form of Validators

Angular supports two types of primary forms:

  • Template-driven forms
  • Reactive forms

Angular forms are the standard forms, but there may be additional fieldssuch as input box, checkbox, select, and so on.

Every field in a form can act as a FormControl that returns complete information about that single field, including whether the form is valid or not, the actual field value, and other information as well.

By combining all of the form controls, we can have a FormGroup, and it’s required that every single form in Angular have one FormGroup.

These are the two form approaches by which we can create forms, form controls, and form groups, Apart from that, we should have form validation so that we can validate our form before sending values to the server.

Custom Validators in Angular

To validate the different form fields, we should have some business logic so that we will be able to send the valid values to the server.

Angular supports two types of form validators: in-built validators and custom validators. Let’s go through the list of in-built form validators in Angular.

  • Min
  • Max
  • Minlength
  • Maxlength
  • Required
  • Compose
  • Pattern

Other validators are also supported apart from these inbuilt ones, and we can also create a custom form validator if we have a specific requirement to validate any field in the form.

Creating a Custom Form Validator

As we know, sometimes in-built validators may not fulfill our validation requirement, so we may need to write custom validation logic to create custom form validators.

Custom validators take the value from the FormControl, where every input acts as a FormControl. So let’s create a form along with a validator function.

Create a new file called customvalidator.validator.ts and paste the following function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { FormGroup, AbstractControl } from "@angular/forms";

// To validate password and confirm password
export function ComparePassword(
  controlName: string,
  matchingControlName: string
) {
  return (formGroup: FormGroup) => {
    const control = formGroup.controls[controlName];
    const matchingControl = formGroup.controls[matchingControlName];

    if (matchingControl.errors && !matchingControl.errors.mustMatch) {
      return;
    }

    if (control.value !== matchingControl.value) {
      matchingControl.setErrors({ mustMatch: true });
    } else {
      matchingControl.setErrors(null);
    }
  };
}
typescript

This is our simple custom validator function, which accepts the name of control along with matchingControlName, which basically compares two different fields for our example.

The next step is to use that custom validator function into our component to initialize the form along with the custom form validator name.

Create a new component called myform, open myform.component.ts, and paste the following source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { Component, OnInit } from "@angular/core";
import {
  FormBuilder,
  FormGroup,
  Validators,
  AbstractControl
} from "@angular/forms";
import {
  ComparePassword,
} from "../customvalidator/customvalidator.validator";

@Component({
  selector: "app-myform",
  templateUrl: "./myform.component.html",
  styleUrls: ["./myform.component.css"]
})
export class MyformComponent implements OnInit {
  registerForm: FormGroup;
  submitted = false;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.registerForm = this.formBuilder.group(
      {
        firstName: ["", [Validators.required]],
        lastName: ["", [Validators.required]],
        email: ["", [Validators.required, Validators.email]],
        password: ["", [Validators.required, Validators.minLength(6)]],
        confirmPassword: ["", Validators.required]
      },
      {
        // Used custom form validator name
        validator: ComparePassword("password", "confirmPassword")
      }
    );
  }

  // Getter function in order to get form controls value
  get f() {
    return this.registerForm.controls;
  }

  onSubmit() {
    this.submitted = true;

    // Returns false if form is invalid
    if (this.registerForm.invalid) {
      return;
    }

    console.log("Form Values" + JSON.stringify(this.registerForm.value));
  }
}
typescript

Let me explain what we have done in this component.

  • We have initialized the form using FormBuilder and combined all the form controls using FormBuilder.
  • Along with each and every form control, we have used in-built validators including required, email, and minlength.
  • Apart from those in-built validators, we have used an additional validator, ComparePassword, which accepts the password, confirms it, and returns the result based on the function execution.

This is how we have developed our custom form validator. Let’s implement two more custom form validators for the first name and last name fields.

1
2
3
4
5
6
7
// To validate first name
export function ValidateFirstName(control: AbstractControl) {
  if (!control.value.startsWith("@")) {
    return { validFname: true };
  }
  return null;
}
typescript

In this function, we will get complete form control using AbstractControl, which is the base class for FormControl, FormGroup, and FormArray.

Using that function, we are validating that the string we are using as the first name should start with the @ operator; otherwise, we return null as a result.

1
2
3
4
5
6
7
// To validate last name
export function ValidateLastName(control: AbstractControl) {
  if (control.value.length <= 3) {
    return { validLname: true };
  }
  return null;
}
typescript

This is yet another custom form validator that accepts the last name string and validates that the last name should be more than three characters long.

So far, we have created three different custom form validators:

  • ComparePassword
  • ValidateFirstName
  • ValidateLastName

Before moving to the template section, we need to modify our form group source code into the component like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ngOnInit() {
    this.registerForm = this.formBuilder.group(
      {
        firstName: ["", [Validators.required, ValidateFirstName]],
        lastName: ["", [Validators.required, ValidateLastName]],
        email: ["", [Validators.required, Validators.email]],
        password: ["", [Validators.required, Validators.minLength(6)]],
        confirmPassword: ["", Validators.required]
      },
      {
        // Used custom form validator name
        validator: ComparePassword("password", "confirmPassword")
      }
    );
}
typescript

Now we are done with our custom form validator functions. Our next step is to create visual form into our app template.

Open myform.component.html and paste following lines of code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
	<table>
		<tr>
			<td></td>
		</tr>
		<tr>
			<td>First Name :</td>
			<td>
				<input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
				<div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
					<div *ngIf="f.firstName.errors.required">First Name is required</div>
				</div>
				<div *ngIf="registerForm.get('firstName').errors &&
      registerForm.get('firstName').dirty &&
      registerForm.get('firstName').errors.validFname">
					First name should start with @
				</div>
			</td>
		</tr>
		<tr>
			<td>Last Name :</td>
			<td>
				<input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
				<div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
					<div *ngIf="f.lastName.errors.required">Last Name is required</div>
				</div>
				<div *ngIf="registerForm.get('lastName').errors &&
      registerForm.get('lastName').dirty &&
      registerForm.get('lastName').errors.validLname">
					Last name should be > 3
				</div>
			</td>
		</tr>
		<tr>
			<td>Email :</td>
			<td>
				<input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
				<div *ngIf="submitted && f.email.errors" class="invalid-feedback">
					<div *ngIf="f.email.errors.required">Email is required</div>
					<div *ngIf="f.email.errors.email">Email must be a valid email address</div>
				</div>
			</td>
		</tr>
		<tr>
			<td>Password :</td>
			<td>
				<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
				<div *ngIf="submitted && f.password.errors" class="invalid-feedback">
					<div *ngIf="f.password.errors.required">Password is required</div>
					<div *ngIf="f.password.errors.minlength">Password must be > 6 character long</div>
				</div>
			</td>
		</tr>
		<tr>
			<td>Confirm Password :</td>
			<td>
				<input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
				<div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
					<div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
					<div *ngIf="f.confirmPassword.errors.mustMatch">Password should be same as Confirm password</div>
				</div>
			</td>
		</tr>
		<tr>
			<td>
				<button class="btn btn-primary">Register</button>
			</td>
		</tr>
	</table>
</form>
html

This looks huge at first glance, but don't worry—we'll look at each of the controlsbriefly.

The most important part is the form tag, which denotes that this is a form along with the additional properties, like formGroup, that we have initialized in the component.

Along with the form, we have one action attached to it called ngSubmit. This means that as soon as the form gets validate and submitted, the function onSubmit will be triggered into the component.

In the form, we have different input controls, along with the additional div section in order to show some error into the form, which looks like this.

1
2
3
4
5
<input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />

<div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
	<div *ngIf="f.firstName.errors.required">First Name is required</div>
</div>
html

The div section is used to show the error for the required field validation, which directly comes from the error object of the form group.

But if we have a custom form validator, it's possible that the error object may be different. For that, we can get the errors by using the form name, which can be implemented like this into the template.

1
2
3
4
5
6
<div *ngIf="registerForm.get('firstName').errors &&
      registerForm.get('firstName').dirty &&
      registerForm.get('firstName').errors.validFname"
>
		The first name should start with @
</div>
html

Here we are getting the errors for the specific form of control by using its name and other parameters, like touched, dirty, and validFname, which is our custom error generated directly from the custom validator function.

For the password comparison, we have created an error object called mustMatch into the custom validator function, hence it can be used like this.

1
2
3
4
5
6
7
8
<input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />

<div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
	<div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
	<div *ngIf="f.confirmPassword.errors.mustMatch">
    	Password should be same as Confirm password
    </div>
</div>
html

This is how we have accessed the different errors based on the error object, whether it’s froman in-built validator or from the custom validator function.

Let’s run this complete, full-fledged example and see how it works.

Custom validator initial output

As you can see, we have provided wrong values against the validation criteria, and this is how the error appeared with the form control.

Now let’s provide the wrong password, confirm the password, and see whether the password comparison validator function works or not.

Final app output

Provide incorrect value to each of the form fields, and we can see the error appears showing that our password and confirm password do not match.

We have now implemented complete end-to-end custom form validators using different strategies and structures. Thus we can use different approaches based on the custom validation criteria.

Conclusion

In this guide, we have gone through the different types of validators and understood why we need custom form validator in our Angular application.

Custom form validators are pretty handy when it comes to custom validation logic for the underlying business logic, and I hope this guide will help you to implement custom form validations. Stay tuned for more advanced guides.

26