3 ways to implement conditional validation of Angular reactive forms

Yury Katkov
Angular Soviet
Published in
6 min readJun 19, 2019

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:

Relevant libraries

Notes

  1. ^ For simplicity I don’t unsubscribe here which will lead to a memory leak. In the real applications always use the takeUntil` trick for unsubscribing.

--

--