3 ways to implement conditional validation of Angular reactive forms
Imagine that you have a form with a checkbox and an email field. The email field has to conform to a regex pattern, and be no longer than 250 symbols, no shorter than 5 symbols. When checkbox is checked you want to make that email field mandatory. When checkbox is unchecked, the email field will become optional. I will call it dynamic or conditional validation.
Sounds like a trivial task for Angular. Let’s create an html structure and a typescript code needed. I use the Reactive Forms approach, so be sure to read the documentation.
Setting up
In the html code we have a form which uses formGroup stored in AppComponent.myForm
, and when it’s time to submit the form we will call AppComponent.onSubmit()
method.
In our form we have myCheckbox
and myEmailField
form controls. Also I added a debugging section in order to always know what’s going on with the form.
Let’s take a look at the typescript:
As you can see, the email field has three validators attached: the pattern validator, maximum length and minimum length.
A naïve and buggy approach
After we’re all set, let’s try to implement conditional validation. If checkbox value is true
, we will set the required validator to myEmailField
. To achieve this, we will need to subscribe to valueChanges
observable of our checkbox and use the setValidators
function of the AbstractControl
class¹:
If you ever tried setValidators
and clearValidators
before, you will spot a bug immediately. When I set the required validator on myEmailField
, all existing validators will be lost, so the email filled will not have length and pattern validation anymore. Same is true for clearValidators
: it will remove all validation from the email field.
You can see this behaviour for yourself using this stackblitz example:
Analysis
The problem is that setValidators
replaces the list of validators instead of adding the new ones.
So, if there’s setValidators
, Angular probably has getValidators
as well, right? No, there is no getValidators
, addValidators
or removeValidators
methods available right now. Moreover, these methods will probably never be implemented in the future, and there’s a pretty good reason for that. I explain in the Angular github issue.
Let’s try to come up with the workaround.
Workaround #1: store all default validators
First workaround will be to save the array of the default validators for the email field into a variable. We will use this variable to initialize the form.
When it’s time to make the myEmailField
mandatory, we will just add the Validators.required
to that array using the concat
function:
As you can see, it works very well. If you want, you can also call updateValueAndValidity
method if you want the errors to appear right after the checkbox is set.
Here is the solution, and its refactored version.
Workaround #2: create custom conditional group validator
Saving the validators like we just did can work for the simple forms, but what if you have a bigger form, with more complex conditions? I can assure you that you will end up with tons of redundant code. We don’t want redundant code. We want clarity.
Another way to achieve conditional validation in Angular forms would be to write your own group validator. This validator will check the value of the whole group, including the a checkbox. If the checkbox is set, it will require the myEmailField
not to be empty. Otherwise it will always return null
, which means ‘no errors found’. Since it’s going to be a group validator, there will be no need to call setValidators
on individual inputs.
To add a validator on a FormGroup
, you need to pass second parameter to your FormBuilder.group()
call:
The emailConditionallyRequired
function is our group validator. It accepts a FormGroup as a parameter and it has the access to the value of the whole group on each change:
If the value of myCheckbox
field is falsy, our form validator will say that no problem found. If the checkbox is set, we will use Angular’s Validators.required
function to validate the email field and if it returns an error, we will generate our own error object.
As you can see, the second workaround leads to much less code. However it has its disadvantages. From now on you need to remember that one of the email field validators is not stored in formGroup.get('myEmailField').errors
, because it’s attached to a form group. Remember this when you render the error messages.
Workaround #3: create custom conditional field validator
So, is it possible to overcome the disadvantages of the previous workaround? Let’s try to create the validator for the single email field, which would be aware of its surroundings. Such validator would accept the formControl
parameter of the type AbstractControl
. Each AbstractControl
has access to its parent, so we can go to a form group and then see the checkbox value!
The validator itself will look like this:
Great! It works! Notice, however, that our validator is only triggered when the value of the email field is changed. Why? Because the validator is attached to the email form group and doesn’t watch its parents. If you want to trigger conditional validation when you toggle the checkbox, you will need to subscribe to the value changes of the checkbox and manually trigger validation of the email field:
Still works! Well, how about making our conditional validator a bit more generic? We can pass a predicate to our validator and it will sure look professional and agile. Also, since it’s not bound to email field anymore, we can rename it from emailConditionalValidator
to something like requiredIfValidator
:
I would stop right here if all I need it make the field conditionally required. What if you need to add more validators conditionally? Like, this field has to conform to a regex pattern if the checkbox is checked, and that field’s max value must be X if a certain condition is met?
Easy! Just pass the predicate as first parameter, and the validator — as second parameter. I will give my uber-super-martin-fowler-abstract validator a new name: conditionalValidator
.
Notice how I use it. The predicate that I pass as the first parameter will be checked each time the validation kicks in for the myEmailField
. You can pass any validator as a second parameter as well, so it possible to do all kinds of dynamic validation, for example this:
You can see the final version here. In my opinion it’s the best we can do to dynamically validate the fields:
Conclusion
It’s easy to create your custom validators for Angular, but because of that simplicity, there is no way to add or remove the validators dynamically. The workarounds are possible. For the simplest forms the workaround#1 can be enough. If your application is big and complicated, I recommend using workaround#3.
Other approaches are possible, for example, you can combine multiple valueChanges
with RxJS, or use setError
function directly. I don’t think that any of them is as elegant and flexible as the workaround#3. Send me a comment
Other links
Here are some resources that I recommend about Angular dynamic validation:
- My explanation why the
getValidators
andremoveValidator
will not be implemented by the Angular team — on Angular’s github - Net Basal, Reactive forms: Tips and Tricks
- Brachi Packter, Angular: How to implement conditional custom validation — I got inspired by the approach I saw there when I was working on the workaround #3
- James McGeachie, Angular 2: Conditional Validation with Reactive Forms, describes something similar to workaround#1
- Pascal Precht, Custom Validators in Angular
- Asim’s Custom Form Validators
Relevant libraries
- RxWeb/reactive-form-validators library includes a nice set of conditional validators that accept predicates, see this article for details.
- ngx-custom-validators — a lot of validation functions you may need in addition to the standard angular validators.