Introduction to Forms in Angular 4: Writing Custom Form Validators

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.

<input name="fullName" ngModel required>

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

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.

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.

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:

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

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:

{
mismatch: true
}

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.

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.

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

<ng-container *ngIf="password.errors?.mismatch;
then first else second"> </ng-container>
<ng-template #first>
Password do not match </ng-template>
<ng-template #second>
Password needs to be more than 8 characters
</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

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:

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.

Attach the directive to the template form control.

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

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

The directive is now attached to all input controls that have the attributes ngModelGroup and passwordMatch.

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.

Demo for Custom Validators in Template-Driven Forms

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.