Throughout your adventures with Angular, you will undoubtedly come across situations where custom directives make sense.
These situations typically involve DOM manipulations, or calling a jQuery plugin.
However, there are other cases where you may not recognize the need to write directives. In this post I will present a
problem that will first be solved without custom directives. I will then provide some motivation for creating custom
directives. And finally, I will show an implementation for these directives.
Warning: This post was written for AngularJS 1.4 and may contain outdated information.
Please see this post
for things to watch out for when reading older Angular posts. If you would like me to update the content of this post,
please ping me on Twitter at @jay_soo. If there are enough interest, I might
make an update.
In this post we’ll cover:
When writing a custom directive makes sense.
Designing directives using a top-down approach.
Allowing communication with your directives through controllers.
Since the introduction of ngMessages and asyncValidators in AngularJS 1.3, I’ve written a
new post
to cover form validations. The designing process in this post is still valid, but I would recommend using the new
AngularJS features to handle error messages and async validations.
The problem at hand
Here is a typical way you may handle form validations in Angular.
HTML:
JavaScript:
Here, we are showing validation errors when a required field is dirty and missing.
This works pretty well as a starting point, but there are a few problems here:
Server errors are not reported (say when the username is already taken).
The error messages are hardcoded within the HTML.
The HTML cannot be reused by other forms, thus violating the DRY principle.
To show server errors, I can have SignUpCtrl stuff them on the scope, but then I will need to modify my HTML to
display these errors. This doesn’t help reusability of our form behaviour.
Designing with semantics
Let’s step back a bit, ignore Angular forms, and imagine how we actually want to create forms
with validation support.
Re-imagined HTML:
I switched the original field errors markup with <fielderrors> custom elements, which specify the input fields
they are responsible for. I also added a marker with-errors to the <form> element. This is to create a parent
directive that will broker communication between ngModel validation and the <fielderrors> elements.
This revised markup makes sense from a semantics point of view. A developer reading the HTML should be able to infer
what the purposes of the new markers are.
Acceptance criteria
Here are the three acceptance criteria that I can think of so far.
We want to invalidate an <input> field and its corresponding <form> when the required attribute
is violated – Angular has this covered!
We want to display error messages inside <fielderrors> if the corresponding <input> is invalid.
We want to support adding additional error messages as needed from a controller (e.g. server errors).
Game plan
We will create three directives:
A directive for the with-errors marker that allows fielderror controllers to register themselves with.
A directive on <input> elements that will watch for Angular validation errors and set the appropriate field errors
via the with-errors directive.
A fielderrors directive that exposes methods for the with-errors directive to set and clear errors. This
directive is also responsible to displaying the error messages on itself.
The one piece missing still is the ability to set additional errors from a controller. We have a couple of choices:
Have with-errors or fielderrors watch for additional errors on their scopes. The downside of this approach is
that we will have to add an additional responsiblity to either with-errors or fielderrors directive.
Create a service that we can call from SignUpCtrl that will set additional errors for the given form by name.
I’m opting for the latter option because it is much cleaner and it means we can avoid another $watch call.
Putting this all together gives up the following diagram.
Test first
First things first, let’s write our Jasmine tests to make sure our implementation satisfies the requirements.
Implementing the setFormErrors service
We start with our setFormErrors service, which allows the withErrors directive to register themselves with.
The exposed service function will set field errors for a given form name.
Implementing the withErrors directive
The withErrors directive controller allows fielderror controllers to register themselves with. It also registers
itself with the setFormErrors service so additional errors can be set without Angular validations.
Lastly, it provides two methods for the input directive to call whenever it encounters an Angular validation error.
Implementing the input directive
The input directive requires the ngModel and withErrors directive controllers. If they are both present, then
it will listen for any errors on ngModel, map those errors to messages, and set those messages using the withErrors
controller.
Implementing the fielderrors directive
Finally, the fielderrors directive requires a parent withErrors directive controller which it registers itself
with. It also provides methods for setting and clearing errors.
Wiring up the controller
Now, our controller may pass additional errors to the fielderrors directive by calling the setFormErrors service.
Try it
Wrap-up
In this post we went through a discovery process for a component in our application that we would like to generalize
and reuse. We started with a high-level design and gathered the acceptance criteria for our component. From our
acceptance criteria, we wrote our tests and finally implemented the component to satisfy those tests.
In reality, there’s a lot of back and forth between design and implementation. This is the way it should be! What I am
proposing is for developers to think top-level design first and implementation second.