In this post, I want to explore a different approach to writing directives in
Angular 1. As we know, building applications in Angular 2 is going to be
different from what we’re used to in Angular 1. For example, ng-controller will
be gone, and components will be the building blocks of applications.
What exactly is a component? It is essentially a thing that encapsulates an
internal state and manages external interactions. The interactions could be with
the user or with other components.
Since I’ve been working heavily with React
recently, I want to see how we can borrow some ideas from there and apply them
to Angular 1 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.
Before We Begin…
I will be writing directives in the following pattern. Please read the comments
for reasoning.
So without further ado, let’s begin.
The Container
The idea is simple. A container is responsible for data fetching and passing data
down to its child components to render. The components are concerned with
rendering the UI based on the data passed down. They can also handle UI interactions.
Notice that services only interact with containers and never components. Essentially,
containers are the data layers of the application.
Why is this pattern useful? Separation of concerns of course!
A Concrete Example
Say, we have the following component.
This code definitely works. But what if we want to reuse the component using different
services, or no service at all? Dependency injection in Angular helps,
but they are not always necessary nor the best solution.
We can instead separate the data fetching concern with the rendering concern.
What happened here? Well, we definitely introduced more code. The main benefit
here though is that we can easily test the message rendering without having
to provide test doubles.
The other advantage of this approach is that we can plug the component into
other containers. As long as the container passes data through the user
attribute to userGreeting then everything will just work!
Interaction with data services
Certain interactions with the UI should affect application data. Form submissions
may require new resources to be created.
A component can accept callbacks that will be invoked when certain events
happen. In Angular, this is done with the & attribute on an isolate scope.
The container can pass its handlers to child components as callbacks,
but interaction with data services still reside in the container.
Let’s say we want to add a feature for users to double-click on their name in the
greeting to edit it. When a double-click event occurs, the name is replaced
with an input box where the user can type in a new name and hit Enter to save.
The editable name component is as follows.
Now, in the userGreeting directive, we replace the user name
with the new UI directive. We also need to chain the onSave callback
from container to the child editableUserName directive.
And finally, we pass the callback from the container, which handles actual
service invocation.
Note: The editableUserName component never modifies
its own state directly (e.g. set this.user). Instead, when the
userGreetingContainer.handleSave() method is resolved, the container updates
its own state. And since it passes the user object to components,
the components will get the updated object automatically.
Phew, we’re done! Below you will find the finished product.
Feel free to fork it and play around
with it yourself.
Summary
We can group directives into two types: container and UI components.
Containers are the data layers. They handle interactions with data services and pass data to components.
Components render data, and they do not mutate this data.
Containers can pass handlers, that interact with data services, as callbacks to components.