When building large scale forms with the Angular Forms module, the available built-in validators (such as making a field required, etc.) usually won't be sufficient, and so you will have to develop your own custom form validation rules.
In this post, you will learn everything that you need to know in order to implement your own custom form validators, including both synchronous and asynchronous, field-level, form-level, and both for template-driven and reactive forms.
If you have used either the Angular Forms or ReactiveForms modules, you will have noticed that every form has a couple of important properties:
Each form field has its own set of business validation rules: the fields can be mandatory, have a minimum length of 10 characters, etc.
Here is an example of a simple reactive form, with a couple of standard validators:
As we can see, this is a simple login form with an email and password fields. We don't see any business validation rules because this is a reactive form, so all validation rules are defined on the component, and not on the template.
Here is the corresponding component class:
There are a lot of validation rules being used here. Let's go through them one at a time, starting with the simpler ones.
In this particular reactive form, we are using the FormBuilder API in order to define our form fields and validation rules in a concise way.
For example, the email field is marked as mandatory via the Validators.required validator, and it also needs to follow the format of a valid email, due to the use of the Validators.email validator.
These are two examples of the several built-in validators that we have available to use, right out of the box.
A form field validator is a function that the form can call in order to decide if a given form field is valid or not.
A validator function returns true if the form field is valid according to the validator rules, or false otherwise.
A validator can be plugged in directly into a reactive form simply by adding it to the validators list. Each form field has its own list of separate validators.
If you noticed, the validation rules for the email and the password fields were configured in a slightly different way.
The password field was configured in the way that you are probably the most used to, by using the FormBuilder API simplified array syntax:
With this syntax, each form field has an array containing both the initial value of the form field and its list of synchronous validators.
Although this syntax is compact, it might become a bit difficult to read for fields with a larger number of validators.
An alternative syntax is the one used by the email field:
In this alternative and more powerful syntax, but also slightly more verbose, the second value of the configuration array (after the initial form field value) is a configuration object containing 3 properties that we can optionally use:
Our advice is to switch to the more powerful syntax with the configuration object as soon as you need to, either to set the updateOn property or to make the form field definition more readable.
Notice also, in the login reactive form, that next to the standard built-in required and minLength validators of the password field, we have a non-standard validator createPasswordStrengthValidator().
This is a custom validator, that we have written ourselves.
The password strength validator ensures that the password has enough strength besides a minimum length. The password strength might be used to require the password to contain not only lowercase characters but also upper case and numeric characters, for example.
With reactive forms, writing a custom validator is as easy as writting a new function:
Let's break down what is going on here. As we can see, the createPasswordStrengthValidator() function is not the validator function itself.
This is actually a validator creation function, that returns as its output the validator. So it's a function that returns a function, as we can see this by its return type ValidatorFn.
The validator creation function can have any arguments needed to set up the validator (in this case no arguments were needed), and returns as the output the validator function itself.
The validator function is then going to be called by the form in order to determine if the form field value is valid or not.
And as we can see, the validator function needs to follow certain conventions.
The form field validator function, returned by the validator creation function, needs to follow these rules:
In the particular case of our password strength validator function, we are checking for the presence of all required characters, and in the end, we are either:
As we can see, we are only returning a boolean flag in the error object indicating that an error was found, without more details. But we could instead also return an error object similar to this one:
This ValidationErrors object can have any form that we need. We can return nested objects if we need to, in order to convey all the necessary information about what caused the error.
Once we have a new custom validator function written, all we have to do is plug it in our reactive form:
Let's remember, the createPasswordStrengthValidator() function did not need input arguments, but we could have defined any arguments that we needed in order to perform the validation, and pass them in this function call.
In order to inform the user that there is a problem with the password strength, we can add the following message to the template:
As we can see, the ValidationErrors object that we returned as the output of our custom validator was added to the pasword field errors object.
If the password is not strong enough, the ngIf condition will turn to true and the error message will then be shown to the user.
For template-driven forms, it takes a bit more work to define a custom form field validator.
With template driven forms, all the business validation rules are defined at the level of the template using directives, and not at the level of the component class.
For example, here is what the same login form would look like in its template-driven version:
As we can see, in the case of template-driven forms, there is a lot more going on in the template.
For example, we can see the standard required, minlength, maxlength and email directives applied to the email and password fields.
The way that these directives work, is that they use internally the corresponding validator functions Validators.required, Validators.minlength, and Validators.maxLength.
Notice also the non-standard passwordStrength directive, applied to the password field. This one we will have to build ourselves!
So in the case of template-driven forms, besides the validator function, we also need to create a custom directive to go along with it.
Otherwise, we won't be able to use our custom validators in our template-driven forms.
Let's have a look at what the custom form field passwordStrength directive looks like:
Let's break this down step by step, starting with the implementation of the validation functionality itself:
But implementing the Validator interface is not enough to make this a valid custom validation directive. We also have to configure it properly in the Angular dependency injection system.
The Angular Forms module needs to know what are the multiple custom validation directives available. It does so by asking the dependency injection system for the NG_VALIDATORS injection token (a sort of unique dependency injection key, that uniquely identifies a dependency).
This dependency in of type multi:true meaning that it includes multiple values, and not just one value. This is to be expected, as there are many custom validator directives, and not just one.
We can see in our directive declaration that we are creating a new provider, meaning that we are setting up a new value in the DI system and we are making the PasswordStrengthDirective available to be instantiated by the DI system if needed.
Because the NG_VALIDATORS provider is of type multi:true, we are adding the PasswordStrengthDirective to the list of existing custom validation directives, instead of replacing the new list with a single value.
Notice that adding the proper DI configuration to your custom validation directive is essential for it to work. Without this, the validate() method simply won't get triggered.
As we can see, it takes a bit more work and also more general Angular knowledge to implement custom validation in template-driven forms.
With reactive forms, all we have to do is to write a function. But in template-driven forms, besides implementing a Validator function, we also have to know how to write our own custom directive and know how to configure the dependency injection system.
But even though things are a bit harder in template-driven forms, it's still quite doable.
If you want to learn more about the differences between template-driven and reactive forms, you can check out our previous article.
Besides being able to define custom validators at the level of each field, we can also define custom validators at the level of the form.
This is very useful in order to do for example multi-field validation, when a validation rule can be true or false depending on the values of multiple different form fields, as opposed to just one field.
Here is an example, imagine a reactive form that takes in two separate input dates, a start date and an end date:
In order for the form to be considered valid, it's not enough to make the two dates mandatory. We also need to put in place a validation rule that says that the start date should be before the end date.
And this can't be done with a custom validator at the level of the form field. Instead, we need to define a validator at the level of the form.
And we can do so also by defining a custom validator function, in a similar way to field-level validators.
Here is an example of a form-level validator for checking if the start date is before the end date:
As we can see, defining a form-level validator is almost the same as a field-level validator! The only difference is that the input of the function is a FormGroup object instead of a form control.
Using the FormGroup, we can easily access any value of the form and compare multiple values together, like for example the start date and end date fields.
Once the form-level validator function is written, all we have to do it is to apply it in the configuration of the form:
As we can see, this type of validator needs to be configured at the level of the form group as a separate configuration object, and not at the level of any of the form fields.
This form-level configuration object can also take the asyncValidators and updateOn property, which we will cover in a moment.
All of the custom validators that we have been showing so far are synchronous, meaning that the new validity state is immediately calculated by the validator as soon as the validator function is called.
For the case of simple validators like mandatory, minimum length, password strength etc. this makes sense, but there are situations where we would like for this new validity state to be calculated in an asynchronous way.
Imagine that in order to determine the new validity state, we would need to perform some sort of asynchronous operation, such as for example calling a backend or using any sort of promise-based library.
In that case, synchronous validators would simply not work. One such example would be the case of the email field. In the case of a user creation form, we want the form button to be enabled only if the user email is not yet used on the backend.
In order to check if the user already exists, we need to call the backend and query the database, which requires an asynchronous operation, and that's where asynchronous validators come in.
Here is what our user creation form would look like:
As we can see, an error message will be shown to the user informing that an user with that email already exists, if the property userExists is present at the level of the field errors object.
Let's now see what the component class looks like, focusing on the email field only:
Synchronous and asynchronous validators cannot be mixed, so that is why we have used the more powerful configuration object syntax to bind an asynchronous validator to the email field.
We are passing to the validator creation function userExistsValidator an input argument of type UserService. This is the service that we are going to be using to call the backend and check if a user with the same email already exists.
And here is what our userExistsValidator asynchronous validator looks like:
As we can see, this is very similar to a normal validator like we have implemented before. The main difference is that the type returned by the validator creation function userExistsValidator() is of type AsyncValidatorFn.
This means that the validation function itself needs to return either a promise or an Observable that emits a value of type ValidationErrors.
In the case of our implementation, we have used Observables, by calling the HTTP based UserService that called our backend and checked if the user exists on the database.
The last piece of the puzzle for fully understanding custom validators, is that updateOn property that we have used on the email field.
The updateOn property can be used both at the level of the form control, like the email field, or at the level of the whole form, like in the case of the start date / end date example.
The updateOn property is useful to define when the form value should be updated with the latest form field value.
When we link a form control such as a text input box to a form model, either by using ngModel, formControl, or formControlName, what happens is that the value of the form control will start to be tracked by the parent form.
When the user starts typing new values or changing the value of the form field, that new value will then be communicated back to the parent form.
The parent form will then need to update its global form.value property, but it will also need to determine if the new value is valid or not.
And for that, it needs to call the validators of the form field and calculate a new field validity state, which will then be used to calculate the new form validity state.
The question is, when will the new value of the form field be communicated back to the parent form?
This is important because by default the value is communicated as quickly as possible. For example, in the case of an input text field, this will by default be communicated with each key pressed by the user, which might be too fast for certain scenarios.
For example, in the case of our userExistsValidator, this would be too much because a new HTTP request to the backend would be triggered with each key pressed.
Instead, we would like the request to be done to the backend only when the user finishes typing the email, so that is why we have set the updateOn property to blur.
This way, only when the user finishes interacting with the email field and moves to another field (either by tabbing away or clicking elsewhere), will then a single HTTP request be triggered to validate the email.
Note that while the asynchronous validation is in progress, the ng-pending css state class will be applied to the email field.
This can be used for styling the email field and informing the user that the validation is in progress if needed.
Besides asynchronous validators, we might also want to set updateOn to blur due to the behavior of certain plain synchronous validators as well.
For example, something as simple as the email validator might not work well if it gets triggered with every key pressed.
When the user starts typing an email, the email will not immediately be valid, which might cause an error message like "The email is invalid" to be shown to the user while he is still typing and then disappear as the email is fully typed, which might be a bit awkward for the user.
There are three values that we can set for the updateOn property:
change: this is the default value. It means that the form will be updated with every new field value (causing all field validators to be triggered). For a select box or checkbox this will happen when the user chooses a new value, but for a text box this will happen with every key pressed
blur: this means that the form will be updated with the new field value only when the field is blurred, meaning that the user either tabbed away or clicked elsewhere on the page
submit: this is rarely used, but its available if needed. This means that the form will only be updated with the new value immediately after the form gets submitted.
The updateOn property can be defined both at the level of the form field and also at level of the overall form itself.
Let's now quickly summarize everything that we have learned about custom form validators:
a custom validator is a function that validates the value of a form field according to a business rule, such as form example the field is mandatory or the password needs to have a certain strength
With reactive forms, it's much easier to define custom validators. All we have to do is to define a function, and plug it into the form configuration.
with template-driven forms, it's also possible to do custom form validation but it's a bit trickier. We will have to define besides the validator function a custom directive and plug it into the dependency injection system
Validators can also be defined at the form level, and this is especially useful for writing validators that depend on the values of multiple fields, such as for example to ensure that the password and confirm password fields in a user creation form are identical
certain validators can need to perform an asynchronous operation, like calling a backend or a promise-based library. This can be implemented by having the validator return either a promise or an observable
For validation to work properly, it's usually necessary to fine-tune exactly when the validators of a field are triggered. This can be done using the updateOn property
I hope that you have enjoyed this post, if you would like to learn a lot more about Angular Forms, we recommend checking the Angular Forms In Depth course, where validators, custom form controls, and many other advanced form topics are covered in detail.
Also, if you have some questions or comments please let me know in the comments below and I will get back to you.