Complex communication between multiple AngularJs directives

Complex communication between controllers and directives (and child directives) is a common problem or challenge in large AngularJs applications (and all large MVVM applications in my opinion).
It becomes even more complicated when you have to communicate with a directive, that has an isolated scope, within a directive that has another isolated scope.

Communication with an isolated scope

When I create a directive that has an isolated scope, AngularJs provides three ways of communicating with it. I can pass in a string variable (@). I can pass in an object with two-way binding (=). Or i can pass in an external function (&).

In most cases these three options are enough. But when your directive gets bigger and more complex -possibly having multiple child directives- things can get a little tricky. If you're not careful you'll end up with a directive that resembles the example below (not a real life case, just an example :)):

<personal-calendar months="data.months"
                   dates="data.dates"
                   events="data.events" 
                   weeks="data.weeks"
                   translations="data.translations"
                   is-approver-for="isApproverFor"
                   event-click="eventClicked()"
                   event-selected="eventSelected()"
                   is-admin="isAdmin"
                   <!--and more and more and more -->
                   ng-show="hasData">
</personal-calendar>

Now, there is nothing wrong with a directive like this. It has a very clear contract that defines how a controller can access it. Which is always a good thing.

But this approach is also a lot of work and a lot of typing. If, for example, this directive has a child directive that needs the eventClicked function, you'll need to pass that function in the personalCalendar directive. And you'll have to do that for each object you want to pass along. To all children that require it.

Your code can quickly become a haystack that does nothing more than taking objects and passing them along to another object. Which becomes a nightmare when you want to refactor things.

Introducing the accessor object

I've used this approach a couple of times now, and I really love it. It really simplifies things. The idea is that you pass one object that becomes the contract. This object can then be re-used in all child directives. You'll have a lot less code-duplication and you don't need to pass individual objects.
Take a look at the following example.

We define the contract in the controller, link the functions and feed the data:

var accessor = {
     months: data.months,
     dates: data.dates,
     events: data.events,
     weeks= data.weeks,
     translations= data.translations,
     isApproverFor: isApproverFor(),
     eventClick: eventClicked,
     eventSelected: eventSelected,
     isAdmin: isAdmin
}

var self = {};
self.isApproverFor = function () { return true; };
self.eventClicked = function (event) { 
    //do something with eventClicked 
};
self.eventSelected = function (event) { 
    //do something with eventSelected 
};

And pass it along to the directive like so:

<personal-calendar accessor="accessor"
                   ng-show="hasData">
</personal-calendar>

Now we have something really powerful. The two way binding mechanism allows us to access everything defined on the accessor within the directive. Watches or event broadcasts are less necessary. You can pass this object to any child directive -and children of children- without problems, they can all use it.

Let's summarize

I've used this approach in a couple of projects now, and I really like it. But there are some cons as well. I wouldn't use it for all my directives, but once things get to complex, it is a golden approach!

Advantages

  • No code duplication and less code all-together
  • Cleaner code
  • Clearly defined contract
  • Less need for watches and broadcasts
  • Cleaner HTML
  • Refactoring becomes less problematic

Disadvantages

  • It is not always clear where a function is invoked or an object is used. When you have many child directives -which have child directives of their own- it is not always clear where certain properties of the accessor object are used or altered. I'm not sure that Angular's 'normal' way of feeding isolated scopes (like in the first example with all the objects) is more clearer than this approach, but it is still a problem.

Things to keep in mind when using this approach

  • Keep your accessor objects as small as possible. Sometimes it is better to split them up, which makes things clearer and less bloated. You could use an eventsAccessor and a dataAccessor in the provided example.
  • Watch out for data initialization! Your directive may be created well before the accessor is defined or feeded. If needed, broadcast an event from your controller when you want to allow your directives to start doing their things on the accessor.
  • Make sure you dispose the object once it is no longer needed. I have yet to see a case in which this approach creates memory leeks, but it is always a good idea to clean up when the object is no longer needed.