1. Code
  2. JavaScript
  3. Angular

Introduction to Forms in Angular 4: Writing Custom Form Validators

Scroll to top
This post is part of a series called Introduction to Forms in Angular 4.
Introduction to Forms in Angular 4: Reactive Forms

This is the third part of the series on creating forms in Angular. In the first two tutorials, we used Angular's template-driven and model-driven approach to create forms. However, while detailing both the approaches, there was something that we didn't cover—custom validator functions. This tutorial will cover everything you need to know about writing custom validators that meet your requirements.

Prerequisites

You don’t need to have followed part one or two of this series for part three to make sense. However, if you are entirely new to forms in Angular, you should head over to the first tutorial of this series and start from there. 

Otherwise, grab a copy of this code from our GitHub repo and use that as a starting point.  

Built-in Validators

Angular doesn't boast a huge built-in validator library. As of Angular 4, we have the following popular validators in Angular:

  • required
  • minlength
  • maxlength
  • pattern

There are actually a few more, and you can see the full list in the Angular docs

We can use the above built-in validators in two ways:

1. As directives in template-driven forms.

1
<input name="fullName" ngModel required>

2. As validators inside the FormControl constructor in model-driven forms.

1
name = new FormControl('', Validators.required)

If the above syntax doesn't make sense, follow my previous tutorials on building a signup form using a template-driven approach or a model-driven approach and then drop back!

The built-in form validators hardly cover all the validation use cases that might be required in a real-world application. For instance, a signup form might need to check whether the values of the password and confirm password control fields are equal and display an error message if they don't match. A validator that blacklists emails from a particular domain is another common example. 

Here is a fact: Template-driven forms are just model-driven forms underneath. In a template-driven form, we let the template take care of the model creation for us. The obvious question now is, how do you attach a validator to a form?

Validators are just functions. In a model-driven form, attaching validators to FormControl is straightforward. In a template-driven form, however, there is a bit more work to be done. In addition to the validator function, you will need to write a directive for the validator and create instances of the directive in the template.

Diving Into the Details

Although this has been already covered, we will go through a quick recap of the code for the signup form. First, here's the reactive approach.

app/signup-form/signup-form.component.ts

1
 // Use the formbuilder to build the Form model

2
    this.signupForm  = this.fb.group({
3
		email: ['',[Validators.required,
4
					Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$')]],
5
		password: this.fb.group({
6
			pwd: ['', [Validators.required, 
7
					   Validators.minLength(8)]],
8
			confirmPwd: ['', [Validators.required,
9
							  Validators.minLength(8)
10
							  ]]
11
		},  { validator: PasswordMatch }),
12
		gender: ['', Validators.required],
13
	})

FormBuilder is a syntax sugar that creates the FormGroup and FormControl instances. A FormControl tracks the value and the validation status of an individual form element. A FormGroup, on the other hand, comprises a group of FormControl instances, and it tracks the value and validity of the whole group.

Here's the structure that we have been following:

1
FormGroup -> 'signupForm'
2
    FormControl -> 'email'
3
    FormGroup -> 'password'
4
        FormControl -> 'pwd'
5
        FormControl -> 'confirmPwd'
6
    FormControl -> 'gender'
7
    

Depending on the requirements, we can attach a validator to a FormControl or a FormGroup. An email blacklisting validator would require it to be attached to the FormControl instance of the email. 

However, for more complex validations where multiple control fields have to be compared and validated, it's a better idea to add the validation logic to the parent FormGroup. As you can see, password has a FormGroup of its own, and this makes it easy for us to write validators that check the equality of pwd and confirmPwd.

For the template-driven form, all that logic goes into the HTML template, and here is an example:

app/signup-form/signup-form.component.html

1
<form novalidate 
2
    	(ngSubmit)="onFormSubmit(signupForm)" 
3
		#signupForm="ngForm">
4
        
5
	<!-- Email input block -->
6
	<input type="text" 
7
	    [ngModel] = "user.email" name="email"
8
        pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$"
9
        required>
10
11
	<!-- Password block -->
12
	<div ngModelGroup="password">
13
	    <input type="password"
14
		        ngModel name="pwd"
15
	            minlength ="8" required
16
	           >
17
	    <input type="password" class="form-control"
18
		        ngModel name="confirmPwd"
19
	           >
20
	    
21
	</div>
22
	   
23
...
24
25
	<!-- Select Gender block -->
26
    <select id="select" class="form-control"
27
          [ngModel] = "user.gender" name = "gender"
28
          required>
29
       
30
        </select>
31
    </form>

ngModel creates an instance of FormControl and binds it to a form control element. Similarly, ngModelGroup creates and binds a FormGroup instance to a DOM element. They share the same model domain structure discussed above. 

It's also interesting to note that FormControl, FormGroup, and FormArray extend the AbstractControl class. What this means is that the AbstractControl class is responsible for tracking the values of form objects, validating them, and powering other things such as pristine, dirty, and touched methods. 

Now that we are acquainted with both the form techniques, let's write our first custom validator.

Custom Validator Function for Model-Driven Forms

Validators are functions that take a FormControl/FormGroup instance as input and return either null or an error object. null is returned when the validation is successful, and if not, the error object is thrown. Here's a very basic version of a validation function. 

app/password-match.ts

1
import { FormGroup } from '@angular/forms';
2
export function passwordMatch(
3
    control: FormGroup):{[key: string]: boolean}  {
4
5
   }

I've declared a function that accepts an instance of FormGroup as an input. It returns an object with a key of type string and a true/false value. This is so that we can return an error object of the form below:

1
{
2
mismatch: true
3
}

Next, we need to get the value of the pwd and confirmPwd FormControl instances. I am going to use control.get() to fetch their values. 

1
export function passwordMatch
2
(control: FormGroup):{[key: string]: boolean}  {
3
    
4
    //Grab pwd and confirmPwd using control.get

5
    const pwd = control.get('pwd');
6
    const confirmPwd = control.get('confirmPwd');
7
   
8
}

Now we need to make the comparison and then return either null or an error object.

app/password-match.ts

1
import { AbstractControl } from '@angular/forms';
2
export function passwordMatch
3
(control: AbstractControl):{[key: string]: boolean}  {
4
    
5
    //Grab pwd and confirmPwd using control.get

6
    const pwd = control.get('pwd');
7
     const confirmPwd = control.get('confirmPwd');
8
      
9
    // If FormControl objects don't exist, return null

10
    if (!pwd || !confirmPwd) return null;
11
    
12
    //If they are indeed equal, return null

13
 	if (pwd.value === confirmPwd.value) {
14
   	  return null;
15
    }
16
   //Else return false

17
   return {
18
      mismatch: true };
19
   }

Why did I replace FormGroup with AbstractControl? As you know, AbstractControl is the mother of all Form* classes, and it gives you more control over the form control objects. It has the added benefit that it makes our validation code more consistent.

Import the passwordMatch function in the SignupForm component and declare it as a validator for the password FormGroup instance.

app/password-match.ts

1
import { passwordMatch } from './../password-match';
2
.
3
.
4
.
5
6
export class SignupFormComponent implements OnInit {
7
8
ngOnInit() {
9
10
11
    // Use the formbuilder to build the Form model

12
    this.signupForm  = this.fb.group({
13
		...	
14
        password: this.fb.group({
15
			pwd: ['', [Validators.required, 
16
		        	   Validators.minLength(8)]],
17
			confirmPwd: ['', [Validators.required,
18
					  Validators.minLength(8)
19
								  ]]
20
			},  { validator: passwordMatch }),
21
		...
22
		})
23
  
24
   }
25
}

Displaying the Errors

If you did everything right, password.errors?.mismatch will be true whenever the values of both the fields don't match.

1
{{ password.errors?.mismatch } json }}

Although there are alternative ways to display errors, I am going to use the ngIf directive to determine whether an error message should be displayed or not.

First, I am going to use ngIf to see if the password is invalid. 

1
    <!-- Password error block -->
2
       <div *ngIf="(password.invalid && password.touched)">
3
     
4
      </div>

We use password.touched to ensure that the user is not greeted with errors even before a key has been pressed.

Next, I am going to use the ngIf ="expression; then a else b" syntax to display the right error.

app/signup-form/signup-form.component.html

1
 <ng-container *ngIf="password.errors?.mismatch;  

2
                then first else second"> </ng-container>
3
    
4
    <ng-template #first> 
5
        Password do not match </ng-template>
6
    
7
    <ng-template #second>  
8
        Password needs to be more than 8 characters
9
    </ng-template>

There you have it, a working model of the validator that checks for password equality.

Demo for Custom Validators in Model-Driven Forms

I've added the code for our custom validators demo to a GitHub repo. You can download or clone the project there to try it out.

Custom Validator Directive for Template-Driven Forms

We will be using the same validator function that we created for the model-driven form earlier. However, we don't have direct access to instances of FormControl/FormGroup in a template-driven form. Here are the things that you will need to do to make the validator work:

  1. Create a PasswordMatchDirective that serves as a wrapper around the passwordMatch validator function. We will be registering the directive as a validator using the NG_VALIDATORS provider. More on this later.
  2. Attach the directive to the template form control. 

Let's write the directive first. Here's what a directive looks like in Angular:

app/password-match.ts

1
import { AbstractControl } from '@angular/forms';
2
3
export function passwordMatch
4
(control: AbstractControl):{[key: string]: boolean}  {
5
    
6
    //Grab pwd and confirmPwd using control.get

7
    const pwd = control.get('pwd');
8
     const confirmPwd = control.get('confirmPwd');
9
      
10
    // If FormControl objects don't exist, return null

11
    if (!pwd || !confirmPwd) return null;
12
    
13
    //If they are indeed equal, return null

14
     if (pwd.value === confirmPwd.value) {
15
   	  return null;
16
    }
17
   //Else return false

18
   return {
19
      mismatch: true };
20
   }
21
22
  
23
//PasswordMatchDirective  

24
@Directive({
25
  selector: '',
26
  providers: [
27
   
28
  ]
29
})
30
31
export class PasswordMatchDirective {
32
}

The @Directive decorator is used to mark the class as an Angular directive. It accepts an object as an argument that specifies the directive configuration meta-data such as selectors for which the directive should be attached, and the list of Providers to be injected, etc. Let's fill in the directive meta-data:

app/password-match.ts

1
@Directive({
2
  selector: '[passwordMatch][ngModelGroup]', //1

3
  providers: [ //2

4
    {
5
      provide: NG_VALIDATORS, 
6
      useValue: passwordMatch, 
7
      multi: true 
8
    }
9
  ]
10
})
11
12
export class PasswordMatchDirective {
13
}
  1. The directive is now attached to all input controls that have the attributes ngModelGroup and passwordMatch
  2. We extend the built-in validators using the NG_VALIDATORS provider. As previously mentioned, NG_VALIDATORS is a provider that has an extensible collection of validators. The passwordMatch function that we created earlier is declared as a dependency. The multi: true sets this provider to be a multi-provider. What this means is that we will be adding to the existing collection of validators provided by NG_VALIDATORS.

Now, add the directive to the declarations array in ngModule.

app/app.module.ts

1
...
2
import {PasswordMatchDirective} from './password-match';
3
4
@NgModule({
5
  declarations: [
6
    AppComponent,
7
    SignupFormComponent,
8
    PasswordMatchDirective
9
  ],
10
  imports: [
11
    BrowserModule,
12
    FormsModule
13
  ],
14
  providers: [],
15
  bootstrap: [AppComponent]
16
})
17
export class AppModule { }

Displaying Error Messages

To display the validation error messages, I am going to use the same template that we created for the model-driven forms.

1
<!-- Password error block -->
2
<div *ngIf="(userPassword.invalid && userPassword.touched)">
3
			 	
4
	<ng-container *ngIf="userPassword.errors?.mismatch;  

5
        then first else second">
6
    </ng-container>
7
    <ng-template #first> Password do not match </ng-template>
8
  
9
    <ng-template #second>  
10
        Password needs to be more than 8 characters
11
    </ng-template>
12
</div>
13
                

Conclusion

In this tutorial, we learned about creating custom Angular validators for forms in Angular. 

Validators are functions that return null or an error object. In model-driven forms, we have to attach the validator to a FormControl/FormGroup instance, and that's it. The procedure was a bit more complex in a template-driven form because we needed to create a directive on top of the validator function. 

If you're interested in continuing to learn more about JavaScript, remember to check out what we have in Envato Market.

I hope that you've enjoyed this series on Forms in Angular. I would love to hear your thoughts. Share them through the comments. 

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.