One of the main benefits of Angular is the ability to easily extend HTML with new behaviours through the use of custom directives. Directives allow developers to match markers in the DOM with the intended behaviours.
In this post we’ll cover:
- Why writing semantic and reusable directives is desirable.
- How to build directives on top of semantic HTML elements and attributes.
- How to provide communication between directives via controllers.
- How to provide basic animations using the
$animate
service.
This post assume you have some experience with Angular directives.
Motivation
The first question you may have is why we care about semantics when building directives. I will attempt to answer that question with a simple example of a view switcher.
Here, we are using ng-show
and ng-click
to provide a view switcher between “show” and “edit” modes.
<div ng-init="mode = 'show'"> <div ng-show="mode == 'show'"> <p>Hi, {{ user.name }}</p> <button ng-click="mode = 'edit'"> Edit </button> </div> <div ng-show="mode == 'edit'"> <p> <input ng-model="user.name"> </p> <button ng-click="mode = 'show'"> Done </button> </div> </div>
Wow, we’ve just created a view switcher without writing any JavaScript! We could be happy with this and call it a day, but let’s take a look at a few issues that I see in the preceeding example.
1. Unsemantic Attributes In The HTML
The three attributes, ng-init
, ng-show
, and ng-click
have no semantic meaning in the HTML. While this may not
seem like a huge issue, I would argue that in a larger application semantics allow you more easily discern the
intent of the added behaviours – in this case, view switching.
2. Limited Reusability Of The Component
Because our HTML is wired for a very specific use-case, it is hard to reuse the component in other contexts.
You could use ng-include
to encapsulate the “show” and “edit” partials within the HTML, then use a controller to
provide the templateUrl
to each includes. Doing so is fragile at best, and still prevents reuse in the case
where we have more than two modes.
Maybe we can keep going down this path and use ng-repeat
to allow any number of views and partials, but this
might introduce more problems rather than solve existing problems. So let’s move on.
3. Imperative vs Declarative Views
Our little HTML is imperative in that we are changing the program state (read mode) through
the expression inside the ng-click
attribute. This is going against the spirit of Angular, which encourages
declarative programming over imperative programming.
A Better Approach
Let’s now take a look at a better potential markup for our view switcher.
<views> <view name="show" initial> <p>Hi, {{ user.name }}</p> <button view-target="edit"> Edit </button> </view> <view name="edit"> <p> <input ng-model="user.name" /> </p> <button view-target="show"> Done </button> </view> </views>
From this new HTML we see that there is an outer views
component which holds multiple child view
components.
The initial
attribute of a <view>
denotes the default, and the elements with a view-target
attribute will
activate the view matched by name
.
I hope the contrast between our first and second example is clear. If not, please leave a comment! :)
Implementation Details
Let’s skip ahead a bit and look at the full implementation first. (We’ll go over the details right after!)
We’ve created two directives:
views
- outer component responsible for registration of views and switching between different views.view
- inner component responsible for showing and hiding the corresponding <view> DOM element.
The Outer Directive
The controller of the outer views
directive provides a method $registerView
for registering a view
controller to
itself (we’ll touch on this shortly), and a method $switchTo
for switching to a view by it’s name
.
In the link function, click events on elements decorated with the view-target
attribute is delegated to the $switchTo
method of the controller. This provides the behaviour of showing a view as specified inside the HTML (and hiding all others).
The last line of the link function makes the views controller available on its scope. This is useful if we need to programmatically switch between views inside other application controllers. For example…
The Inner Directive
The view
directive requires two controllers:
- Its own controller which performs the actual show and hide in the DOM.
- A parent
views
directive’s controller to register itself with.
The link function first registers its own controller with the parent, then it checks if it is the initial
view. If so,
it will call the $show()
method, otherwise it calls $hide()
.
Bonus: Going The Distance With Animations
If you are as much of fan of ngAnimate
as I am, you will definitely want to make your directives compatible with the
animation framework.
To make animations work, we can simply switch out the .show()
and .hide()
method calls in the inner view
directive
with $animate.addClass()
and $animate.removeClass()
. We will also need to provide CSS animations for showing and
hiding.
JavaScript:
CSS:
views { position: relative; }
view {
position: absolute;
top: 0;
width: 500px;
}
view.view-hide { display: none; }
view.view-hide-remove, view.view-hide-add { display: block !important; }
view.view-hide-add {
animation: .3s hide;
-webkit-animation: .3s hide;
}
@keyframes hide {
from { opacity:1; }
to { opacity:0; }
}
@-webkit-keyframes hide {
from { opacity:1; }
to { opacity:0; }
}
view.view-hide-remove {
animation: .3s show;
-webkit-animation: .3s show;
}
@keyframes show {
from { opacity:0; }
to { opacity:1; }
}
@-webkit-keyframes show {
from { opacity:0; }
to { opacity:1; }
}
With very little JavaScript changes, and a bit of CSS we’ve just added animations to our directive!
The full example can be found on Plunker.
Wrap-Up, Future-Proofing Your Components
We’ve learned how to extend HTML through the use of Angular directives. You may notice that the final HTML of our
component contains almost no Angular (with the exception of ng-model
). This fits very well with AngularJS 2.0’s goal
of integrating with Web Components and other frameworks, such as
Polymer.
Also note that our final solution allows us to use an arbitrary number of views within our HTML without any hassle!
<views>
<ul>
<li><a view-target="foo">Foo</a></li>
<li><a view-target="bar">Bar</a></li>
<li><a view-target="faz">Faz</a></li>
<li><a view-target="baz">Baz</a></li>
</ul>
<view name="foo" initial>...</view>
<view name="bar">...</view>
<view name="faz">...</view>
<view name="baz">...</view>
</views>
Please keep in mind that the examples shown here are not production-ready, and are missing a lot of pieces, such
cleaning up on scope’s $destroy
event.
I’ve touched a little bit on animations in Angular. If you want to learn more, please do read Year of Moo’s post on animations in AngularJS 2.0, and also refer to the official $animate docs.