Communication between collaborating directives in Angular

Posted on by in Web

Directives man! While the they’re literally the entry point into Angular development (every application contains a call to ng-app), many people starting out with Angular are hesitant to write their own because of the complexity associated with them. And directives are complex. Even a simple directive requires understanding complicated concepts like restrict, scope and link. But when you figure it out, the value of a directive as a reusable component becomes indispensable.

But what about when a directive isn’t self contained? What if a complicated component comes along that is naturally modeled by multiple directives? This group of directives, as a whole, form a single self contained component. None of directives in the group can stand alone because they only make sense when used together; they collaborate; they are aware of each other and need to communicate with each other.

This post will discuss best practices for managing communication among collaborating directives and illustrate these practices with an example.

Imagine a scenario like this:

<parent-component>
  <child-component></child-component>
  <another-child-component></another-child-component>
</parent-component>

Let’s assume that when something changes in child-component that parent-component and another-child-component need to know about it. How should these directives communicate? There is an answer tucked away in the Angular documentation, but before looking at it let’s survey a set of approaches that could be taken with the tools at hand.

Shared scopes

It’s tempting to communicate via scopes. parent-component could expose an API by exporting functions and data on its scope. This API is available to both child-component and another-child-component, assuming neither uses an isolate scope. Now both directives can communicate directly with their parent via the API on scope. However, parent-component cannot communicate directly with child-component or another-child-component because scopes represent a hierarchical relationship between parent and child. This relationship is implemented using prototypical inheritance, so children have access to data placed on the scope by the parent but not vice versa. parent-component has to find another way to communicate with its children.

Similarly, child-component and another-child-component will have trouble communicating because they are siblings and scopes do not provide a method for sibling communication.

This approach does not appear to be ideal. Communication is limited, but there are a few other problems:

  • Scopes are generally used to provide data to templates and coordinate events generated by user interaction with the view. Using them for inter-directive communication concerns overloads the responsibilities of scope. This is working against the framework.
  • Isolated scopes are not available since data and communication is being routed through scopes.
  • As parent-component grows, so too will the surface area of scope. It will become hard to maintain and reason about the implementation. Data placed on the scope by parent-component may accidentally be masked by data placed on the scope by child-component or another-child-component.

If scopes aren’t your jam, check out this article on our blog for a more detailed discussion on scopes in angular.

Eventing via $scope.$broadcast, $scope.$emit & $scope.$on

Because communication between parent and child or between sibling directives is limited via scopes, another option is Angular’s eventing system. Because this approach does not rely on scope (directly at least) for communication none of the problems outlined above will present themselves. But the design of the eventing system presents its own challenges.

Events in Angular are directional. They either travel up the scope hierarchy via $scope.$emit or down it via $scope.$broadcast. This means that the location of a directive in the document is important when it comes to sending the event. While this can be circumvented by injecting $rootScope and only sending events down the chain using $rootScope.$broadcast, the extra dependency is a bit of a bummer. An increasing dependency list size is usually an indication that something is amiss.

The biggest problem with this approach, conceptually at least, is that the collaborating directives are carrying on a private conversation in public. By using $broadcast, the event is delivered across the entire system and any directive can react to it. This can lead to unintended consequences, e.g. a generically named event triggers an unintended event handler. This problem can be solved by naming conventions, but this is a work around attempting to better target the event. $broadcast is best suited to situations where the type and number of listeners for an event is unknown by the origin of the event. This differs from the situation of collaborating directives; the consumers are well known.

Controllers

So how should collaborating directives communicate? The answer is in the documentation for directives (tucked away at the bottom of the page):

Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.

Obvious, right? Ok, maybe not. Taking a look at how a directive is defined will shed some light. Consider, again, this example template:

<parent-component>
  <child-component></child-component>
  <another-child-component></another-child-component>
</parent-component>

And this directive definition for parent-component:

module.directive('parentComponent', function() {
  function ParentComponentController(scope) {
    // initialize scope
  }

  ParentComponentController.prototype.doSomething = function() {
    // ironically does nothing
  }

  return {
    restrict: 'E',
    controller: ['$scope', ParentComponentController],
    scope: {}
  };
});

The parent-component directive specifies it provides a controller ParentComponentController. The controller is a class and is instantiated each time the directive is encountered. It is instantiated before the directive’s template is compiled so that the controller can initialize the scope if needed.

Now look at the directive definition for child-component:

module.directive('childComponent', function() {
  function link(scope, element, attrs, controller) {
    controller.doSomething();
  }

  return {
    restrict: 'E',
    require: '^parentComponent',
    link: link,
    scope: {}
  }
});

There are two things at play here. child-component specifies via the require property that it needs the controller provided by parent-component. Controllers are referenced not by the name of the controller class, but by the name of the directive as registered with the dependency injector. The ‘^’ indicates that the controller is provided by a parent directive. As a result of requiring the parent-component controller, it is passed to child-component‘s link function as the fourth argument.

That covers the basic mechanics of inter-directive communication. The remainder of this article will focus on an example. If you’d like to follow along, keep reading. Otherwise the source for the example is available here.

An Example

It should behave like this:

  • By default, annotations are not shown.
  • The total number of annotations is shown
  • There is a control to show annotations.
  • Pressing the “show annotations” control overlays the annotations on the image.
  • The “show annotations” control is replaced by a “hide annotations” control after it is pressed.
  • Annotations can be selected by clicking on their visual representation in the image.
  • Selecting an annotation will show the text and author of the annotation.
  • Pressing “hide annotations” control returns the component to its original state.

As a starting point, consider what the template for this component might look like:

<div class="annotations-control">
  <span ng-click="showAnnotations()" ng-hide="viewing">Show</span>
  <span ng-click="hideAnnotations()" ng-show="viewing">Hide</span>
  <span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>
</div>

<canvas></canvas>

<div class="annotation-content" ng-if="viewing">
  <div ng-if="annotation">
    <span>"{{ annotation.text }}"</span> - <span>{{ annotation.author }}</span>
  </div>
</div>

The canvas tag will be where the image and the annotations are dispayed. The div with class annotations-control represents the controls area for showing and hiding. The div with class annotation-content is the text of the currently selected annotation. Unfortunately, the template does not specify where the image source and the annotations are coming from. This means anybody using this code will also have to look in the javascript to fully know how this component is wired up.

The canvas and the two divs represent distinct visual components and make good candidates for directives. Let’s update the template to only show the higher level directives:

<annotated-image-controls annotations="annotations"></annotated-image-controls>
<annotated-image-viewer src="image" annotations="annotations"></annotated-image-viewer>
<annotated-image-current></annotated-image-current>

This looks better. Before it was unclear where the image source and annotations came from, now it is expressed in the template. But, this is a lot of boilerplate to use this component. Taking that as a hint, let’s expose a single directive to coordinate the whole thing:

<annotated-image configuration="config"></annotated-image>

The definition of annotated-image looks like this:

angular.module('annotated-image').directive('annotatedImage', function()
{
  function AnnotatedImageController(scope) {}

  return {
    {
      restrict: 'E',
      template: [
        '<annotated-image-controls annotations="configuration.annotations"></annotated-image-controls>',
        '<annotated-image-viewer src="configuration.image" annotations="configuration.annotations"></annotated-image-viewer>',
        '<annotated-image-current></annotated-image-current>'
      ].join('\n'),
      controller: ['$scope', AnnotatedImageController],
      scope: {
        configuration: '='
      }
    }
  };
});

The main responsibility of annotated-image is to coordinate annotated-image-viewer, annotated-image-controls and annotated-image-current. It provides a template which includes them and a controller to facilitate communication between them. The controller will be built up as the other three directives are developed.

Let’s focus on the task of showing annotations. Remember that clicking the “show annotations” control should cause changes in all three directives. Here are their definitions:

angular.module('annotated-image').directive('annotatedImageControls', function() {
  function link(scope, el, attrs, controller) {
    scope.showAnnotations = function() {
      controller.showAnnotations();
    };

    controller.onShowAnnotations(function() {
      scope.viewing = true;
    });
  }

  return {
    restrict: 'E',
    require: '^annotatedImage',
    template: [
      '<div>',
        '<span span[data-role="show annotations"] ng-click="showAnnotations()" ng-hide="viewing">Show</span>',
        '<span span[data-role="hide annotations"] ng-click="hideAnnotations()" ng-show="viewing">Hide</span>',
        '<span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>',
      '</div>'
    ].join('\n'),
    link: link,
    scope: {
      annotations: '='
    }
  };
});
angular.module('annotated-image').directive('annotatedImageViewer', function() {
  function link(scope, el, attrs, controller) {
    var canvas = el.find('canvas');
    var viewManager = new AnnotatedImage.ViewManager(canvas[0], scope.src);

    controller.onShowAnnotations(function() {
      viewManager.showAnnotations(scope.annotations);
    });
  }

  return {
    restrict: 'E',
    require: '^annotatedImage',
    template: '<canvas></canvas>',
    link: link,
    scope: {
      src: '=',
      annotations: '='
    }
  };
});
angular.module('annotated-image').directive('annotatedImageCurrent', function() {
  function link(scope, el, attrs, controller) {
    controller.onShowAnnotations(function() {
      scope.viewing = true;
    });
  }

  return {
    restrict: 'E',
    require: '^annotatedImage',
    link: link,
    template: [
      '<div class="annotation-content" ng-if="viewing">',
        '<div ng-if="annotation">',
          '<span>"{{ annotation.text }}"</span> - <span>{{ annotation.author }}</span>',
        '</div>',
      '</div>'
    ].join('\n'),
    scope: { }
  };
};

Notice annotated-image-controls, annotated-image-viewer and annotated-image-current require the controller provided by annotated-image. Also notice that the fourth argument to the link function is the required controller. Remember that the ‘^’ at the beginning of the require property means the controller will be provided by a parent directive. This effectively means that these directives cannot be used outside of annotated-image because Angular will throw an error when it cannot locate the required controller.

annotated-image-controls exports a function called showAnnotations which is bound via ng-click to span[data-role="show annotations"]. The showAnnotations function simply delegates to AnnotatedImageController.

When AnnotatedImageController#showAnnotations is called it notifies interested listeners of the event. This is implemented using a simple observer pattern for flexibility:

function AnnotatedImageController(scope) {
  this.handlers = {
    showAnnotations: []
  }
}

AnnotatedImageController.prototype.showAnnotations = function() {
  this.handlers.showAnnotations.forEach(function(handler) {
    handler();
  });
}

AnnotatedImageController.prototype.onShowAnnotations = function(handler)
{
  this.handlers.showAnnotations.push(handler);
}

All three directives register themselves with AnnotatedImageController#onShowAnnotations because all three must take action when a request to show annotations is made. annotated-image-controls toggles it’s state. annotated-image-current reveals itself. annotated-image-viewer redraws the image with the annotations overlaid. The “hide annotations” functionality can be implemented in much the same way.

Summary

Directives are complicated. Directives that need to collaborate are even more complicated. Having an understanding of the tools Angular provides in this scenario can help you manage and contain this complexity within your application.


Feedback

  Comments: 23


  1. This is a mind-blowing read and causes a new way of thinking about angular directives – thank you for posting it and being so clear and concise.


  2. I much prefer your third example and the use of controller over broadcasting events and using scope inheritance. However, doesn’t this approach result in child directives that are tightly coupled to their parent directive? E.g in your above example, annotatedImageCurrent, annotatedImageViewer, and annotatedImageControls can only be used with the annotatedImage directive.

    Whilst this seems fine in your above example, what would you do if your annotatedImageControls was quite a generic directive which you wanted to be reusable within multiple parent directives?


    • I think getting caught up in everything being reusable is just asking for more complicated code. I think the author clearly is showing how to create a component with various directives that can help maintain code readability and scalability of that component. His example proves this. There should be no concern here.


    • Exactly what I was thinking. What good is having a child directive that can only have a specific parent?


  3. I really enjoyed the read, however, I would like to second Jaker’s concern. It looks like this tightly couples the child, to it’s parent? I’m not sure if that’s really a significant problem or not… It appears to just limit that directives use to being used in conjunction with the parent.

    That’s probably fine for most situations. But, I think an event bus system provides a bit more flexibility.


    • Event bus system offers a lot of complexity. you know how many people realize pub sub mediators eventually are just overhead in their architectures? Don’t worry too much if some of your components are tightly coupled with their directives, not everything has to be reused. Certain use cases are too much overhead for widgets/components that are very unique in their behavior and presentation.

  4. nachoargentina


    Great article. I stumble upon it when I was searching for answers on whether my directives were communicating correctly betweend themselves.
    The position I’m in right now is that I have 2 directives, the child directive depends on the parent, but the parent directive can live without it’s child directive. What’s the main reason why I can’t put them together in one directive? the fact that their HTML don’t go together. The parent can be at the top of the DOM, then I can have a lot of other content and finally the child directive at the end. That’s why I ended up using the $broadcast approach. Did I make the right choice there? Everyone’s opinion would be greatly appreciated!


  5. This works great but I also have the same question as jaker.


  6. Same question as Martin, Jaker, nachoargentina, and term. Would be nice to hear a response.


  7. I will third and forth the coupling to the parent controller. Events were working pretty good for my purposes, but I was then using the same image-uploader directive multiple pages for different purposes on the page I’m using and needed something to tie them together, I was hoping the Controller method you describe would be my savior, and in some ways it is, but it’s still not scoped according to instances of the directive throughout the page. Any thoughts on this?


  8. @nachoargentina

    “What’s the main reason why I can’t put them together in one directive?”

    If you are using the child directives within the parent directive template, I can’t see a good reason, apart from maybe a bit of code separation.

    If you are allowing the child directives to be optional via transclusion, keeping them separate is compulsory and gives controller communication a compelling reason to exist.

    Regarding tight coupling with require, this is a limitation of the framework. I believe Angular 2 is more flexible with interdirective communication from the little I have seen of it.


  9. Thanks Emily! I’m currently wnoikrg on thoroughly annotating the full transcript of the commands I typed into the terminal during the workshop. This includes all the regular expression examples, the demonstration of the Pitchfork scraper, and the word-frequency stuff we did at the end with the Latin document someone volunteered. That’ll be posted, along with some other goodies, on the workshop web page once I’m finished.


  10. Great post!!

    I would also suggest the third option BUT adding rxjs subjects (https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/subjects.md) to the equation. With that your child directives can literally subscribe to events fired by the parent and register itselves as generators for events targeted to the parent. This way you got the simplicity and decoupling of events with the privacy of using controllers. Because you need the parent controller to subscribe or register.

    Regards


  11. Very nicely written. I have the same question as jaker


  12. I have the same question as jaker. Suspecting the answer will be “Events”.


  13. This is a very useful and beautifully crafted tutorial that very well explains the idea of directive communication via controller API. I’ve managed to create a group of custom directives with my desired behavior in no time after I reading this, though before I felt lost about what is the right way to enable my directives to communicate. Thank you, Jeremy!


  14. Great post! Great great great, to me this proves the superiority of MVVM/Observable architecture, because your behind the scenes observer pattern solution just shows how bad Angular is.


  15. That’s a great article. I’m learning about this just now and this article is very helpful, thank you.


  16. Green Day is a famous country singer, so don’t miss the possibility to visit Green Day concert baltimore

  17. Www.Nearbynursinghomes.Com


    The nurseing care plan is a composed overview that
    ortanizes details concerning a client’shealth and wellness.


  18. Its really great article and it changed my way of thinking entirely. I have gone through blog and example here.
    I have a question. In this approach parent and child directive are using the same controller. Is this mean parent controller will have business logic for its child controllers too. if not then how child will have their own controller along with this approach.
    replay greatly appreciated.

    thanks in advance.

    Lokesh soni

Your feedback