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:

angular directive communication via controller-as

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!

Enjoy this? Subscribe to my RSS feed or follow me on Twitter.