Writing and testing custom Angular validators: the “passwords matching” case

Validators: easy to use, tricky to build…
Well, not that tricky as we’ll see in this post. In my very humble opinion, Angular has done a pretty decent work in making the writing of your custom validators as painless as possible.

In the majority of cases you will not need to write your own validators: Angular took care of that for you for most situations. So when should you write one?

Imagine you are implementing a subscription form and you ask the user to type a password… and then to re-type it just to make sure. You may want to give the user some feedback if he typed a different password the second time, right?

One way of resolving that is to write a custom validator.

Validate this!

Our validator will be bound to a “Repeat Password” field and observe the original “Password” field: it will get the upper field’s value and compare it to its own (the lower one) to establish if the passwords match or not.

The form that renders such a situation might look a bit like this:

There’s nothing surprising in our template above. We just make sure we have a reference to the “Password” field’s model because that’s what we will pass as a parameter to our custom validator, which we have named “fieldMatches“.

So how do we define that “fieldMatches” validator? We must declare a directive for that purpose, like this:

We start by defining the selector property.
The important bit is that a directive requires a CSS selector to point to what is linked to this directive, like when you do input[name="username"] See how we reached the “name” attribute whose value is “username” by using square brackets? In an Angular directive selector we use the brackets to point to the “fieldMatches” attribute. It has nothing to do with Angular’s data binding notation!

Next we register our own validator. The “multi” bit allows us to add our own to the ones that are already provided by Angular.

Two interfaces need to be implemented. you probably already know OnChanges, which we’re going to use to initialize our validation function. The Validator interface requires to implement a validate() function, which is going to be called repeatedly by Angular to see if the field passes this validation test or not.

Let’s go through the sequence of events step by step:

  1. When the form field with fieldMatches is set up, a change event is fired and ngOnChanges() creates the validator function by using our homemade function factory, named fieldMatchesValidator().
  2. What is passed to fieldMatchesValidator() is the NgModel of the other field. This implies that in this implementation the change will be fired only once, essentially to generate our validation function!
  3. Then the user types something in the “Repeat Password” field, therefore the function assigned to this.validateFunction is called and receives the current AbstractControl.
  4. The function assigned to this.validationFunction,  if the initialization took place, is the one containing our validation logic and initially provided by our function factory: fieldMatchesValidator().
  5. Our validation logic gets the AbstractControl object from which it can extract the field’s current value. It compares it with the value from the “Password” field’s model.
  6. If the fields are equal, null is returned. That means “all is well, mate!”.
  7. If the fields are not equal, an object describing what is wrong is returned: ValidationErrors, which is basically an alias on a map. That object is added to the list of errors carried by the “Repeat Password” field’s NgModel.
  8. So now it is possible for your code to look at the “Repeat PasswordNgModel and display something if its errors property carries a fieldMatches issue:

Validate the validator

Right, but now we need to make sure all this works. And because we are clean coders, we are going to write a test for it!

Now we could write isolated unit tests, but these would only really be useful to test our validation logic and that’s only a small part of the equation. Instead, we could use Angular testing utilities to do something similar to an integration test, and see how our validator interacts with Angular and (most importantly) with a template.

We’ll go for the second option. For that we need two things:

  • A fake template that will expose our validator
  • TestBed: a testing utility allowing us to set up an Angular testing module. That module is easily configurable and allows us to create a controlled test environment to try our validator in.

Brace yourself, there goes the code:

Using the TestBed is an extensive topic, one you should really explore if you intend to test your Angular application back and forth. I refer you to the official doc, which has an extensive amount of examples to help you in your testing mission!

Back to our validator test, though. There are two beforeEach() calls: the first one sets up the TestBed, while the second one creates an instance of the component under test.

That test component (named… TestComponent, how original) is merely a component we set up for our test. It defines an in-line template, which declares our validator on one of two fields in a dummy form. It also exposes two properties that are bound to the dummy form’s fields.
That’s the component for which a fixture is returned when calling TestBed.createComponent().

When running, our test uses that fixture to set up the fields’ values using the fixture’s exposed component instance. It waits for all pending asynchronous activities to end… should there be any (not all tests trigger asynchronous processes). It then fetches the second field’s reference to the model, from the fixture (always from the fixture, remember!), and tests if its validation status matches what we expect.

Conclusion

And essentially that’s all there is to it!

Agreed, it might look like a lot of work to check if two fields are the same or not. Nevertheless I find it’s a classy and eminently reusable way of implementing that bit of logic, don’t you think?

Now for a little trivia: if you implement the validator as described, you will notice that in a specific situation you will not get the expected result. Which one, and why?
I’ll let you ponder on this. Muhahahaaaa!

Until then,

Cheers!

 

Leave a Reply

Your email address will not be published. Required fields are marked *