/ AngularJS

Communicating between AngularJS Directives to share data

I have a cstLabel directive which takes a numeric identifier and uses that to lookup the friendly text to show to the user. So the directive would take a dependency a lookupService which can get the data from the server, all nice and straight forward:

angular.module('app')
  .directive('cstLabel', ['lookupService', function(lookupService) {
    return {
      restrict: 'E',
      scope: {
        labelId: '='
      },
      template: '<span>{{labelText}}</span>',
      link: function(scope, element, attrs) {
          var labels = lookupService.getLookup("labels");
          
          scope.$watch('labelId', function (labelId) {
            scope.labelText = labels[labelId];
          });
        }
    };
  }]);

[Not the prettiest code, but just for demonstration purposes :)]

So what's the problem?

Well, if some arbitrary demo Html were to look like the following, then we'd have a problem:

    <ul>
      <li><cst-label label-id="1"></cst-label></li>
      <li><cst-label label-id="2"></cst-label></li>
      <li><cst-label label-id="3"></cst-label></li>
      <li><cst-label label-id="2"></cst-label></li>
    </ul>

When multiple instances of this directive are in the page, they each call the lookupService to get the friendly text. When this text comes from the server, that means a whole heap of requests for the same data!

Hang on, aren't services singletons? Can't you cache the data in the service?

Well you can - in fact, there was caching in place in the app I was working on - but there was a race-condition. Each instance of the cstLabel directive makes a call to the same singleton service, pretty much at the same time, but with a cold cache each call would find the cache empty and go to the server to get the data. This is only really a problem for a cold cache and when lots of data is involved, but I wanted to solve it as easily as possible.

The solution was to share data between the directive instances

This was done using another directive that could be required by the original directive using the require property on the DDO (Directive Definition Object). The new cstLabelContainer directive would be the one to go and get the lookups, with each cstLabel instance able to get that data thanks to the require hierarchy:

angular.module('app')
  .directive('cstLabelContainer', function() {
      return {
        restrict: 'A',
        controller: ['lookupService',
          function(lookupService) {
            this.labels = lookupService.getLookup("labels");
          }
        ]
      };
    })
  .directive('cstLabel', function() {
    return {
      restrict: 'E',
      require: '^cstLabelContainer',
      scope: {
        labelId: '='
      },
      template: '<span>{{labelText}}</span>',
      link: function(scope, element, attrs, labelContainerController) {
          scope.$watch('labelId', function (labelId) {
            scope.labelText = labelContainerController.labels[labelId];
          });
        }
    };
  });

So now, there is only a single call to the lookupService (and so too to the server) and each instance of the cst-label directive still gets the data it needs. Nice.

This changes the Html to be (note the cst-label-container on the ul):

    <ul cst-label-container>
      <li><cst-label label-id="1"></cst-label></li>
      <li><cst-label label-id="2"></cst-label></li>
      <li><cst-label label-id="3"></cst-label></li>
      <li><cst-label label-id="2"></cst-label></li>
    </ul>

Take a look at the Angular docs on directives: Creating Directives that Communicate for more details on how this works.