Directive communication in Angular
Directives in Angular are great. One challenge people face, however, is getting them to interact without creating too much, or too little, coupling.
The route of high-coupling is via the Scope
. However this means sharing a heap of mutable state between components. Worse still, if one directive needs to publish a function for another to use it's completely invisible in the template - and requires heavy setup in tests.
The low-coupling route is to use events (e.g $rootScope.emit()
), or services. However both are best suited to very low-cohesion interactions as they are so implicit: they don't appear in the template and you have to read the source for both to see the publish/subscribe pairing.
For components that interact closely events or services therefore reduce the readability of your code, making it hard for others to see the interactions. Explicit is better than implicit.
The controller-as
pattern for directives
A pattern I really like is to define a controller-as
attribute on directives to explicitly publish their public API. As an example, the following UI has a filter widget and a chart widget. The filter controls what data the graph shows. Their interaction is too direct for events or services to be a good choice:
The HTML we want is something like this. You can see we have revealed the interactions between the components explicitly without any coupling:
<!-- top level controller - responsible for fetching data etc -->
<div ng-controller='DataVizCtrl as ctrl'>
<!-- our filter widget has an on-filtered event to
allow us to pass its filter to other directives -->
<filter-widget
on-filtered='ctrl.graphCtrl.applyFilter($filter)'
</filter-widget>
<!-- graph widget exposes its controller so we can
use its API in our template -->
<graph-widget
controller-as='ctrl.graphCtrl'
data='ctrl.data'
>
</graph-widget>
</div>
Now for the definitions of the directives - I've kept them minimal to keep focussed.
The filter widget defines a on-filtered=
event that is provided with a $filter
variable. This will be a function usable in Array.prototype.filter
etc - a function that takes a data item and decides whether it's included. This is a high level of abstraction over how that filter is constructed from the UI:
function filterWidget() {
// minimal definition - this would have a template
// and more code in reality
return {
scope: {
onFiltered: "&",
},
link: function(scope) {
// called from inside this directive's UI, we
// call the `onFiltered` expression exposing
// local `$filter` variable
scope.publishFilter = function() {
scope.onFiltered({
$filter: createFilter(),
});
}
// returns a filtering function
function createFilter() {
}
}
}
}
The graph widget exposes a controller with its public API via controller-as=
. This allows it to be told about any desired modifications to the data (e.g a filter) and determine how to present them (i.e nice animations):
function graphWidget() {
return {
scope: {
controllerAs: "ctrl",
},
controller: GraphWidgetCtrl,
},
}
function GraphWidgetCtrl($scope) {
$scope.controllerAs = this;
this.applyFilter = function(filter) {
this.filteredData = this.data.filter(filter);
}
}
I find this pattern results in very readable interactions between components, with excellent testability. Hope you find it useful!
Test comment.
Leave a comment via Github 💬