Communication within Angular

Peloton Engineering
Peloton-Engineering
3 min readJun 17, 2015

When scaling an angular application, it can get very confusing as to how you send messages around between controllers, services, directives, etc. We’ve compiled a few guidelines that we use in order to keep our code clean as it scales.

$scope Broadcasts

To be perfectly honest, these are messy. We avoid broadcasting through $scope like the plague. We believe that the point of these events are for very high level angular operations to broadcast their events as an API. Take the ‘$destroy’ event for example, it is provided as a piece of an API so that the developer has a convenient hook to destroy their directive in the appropriate fashion. So then how do we broadcast information across our angular application? Our solution involves rolling our own isolated PubSub mixing throughout our angular services.

Service Broadcasts

Services/Factories/Providers do not come built with any method of broadcasting information. Our first attempt to solve this was to namespace $rootScope broadcasts and send information around that way. While this was the easier solution, we quickly abandoned this approach because we preferred a more explicit way of communicating from services. The solution then became to roll our own PubSub into our service layer via a mixin. The basic PubSub mixin looks like this:

angular.module(‘app’)
.factory(‘PubSub’, [‘$rootScope’, function($rootScope) {
function PubSub() { this.observers = {}; } PubSub.prototype.on = function(name, fn) { this.observers[name] = this.observers[name] || {}; var key = Object.keys(this.observers[name]).length + ''; this.observers[name][key] = fn; return key; }; PubSub.prototype.off = function(name, id) { if (this.observers[name] && this.observers[name][id]) { delete this.observers[name][id]; } }; PubSub.prototype.broadcast = function(name, event) { if (!this.observers[name]) {

return;

}
var observers = this.observers[name]; Object.keys(this.observers[name]).forEach(function(key) { if (!observers[key]) {
return;
}
observers[key](event); }); if (!$rootScope.$$phase) { $rootScope.$apply();

}
}; return PubSub;}]);

To extend this, we include it when we create our service like so:

angular.module(‘app’)
.factory(‘SomeService’, [‘PubSub’, function(PubSub) {
// constructor, implemented like you would an angular service
var SomeService = function() {
PubSub.call(this); }

angular.extend(SomeService.prototype, PubSub.prototype);
return new SomeService();}]);

This then allows us to listen to the broadcast in other locations that would import this.

angular.module(‘app’)
.controller(‘SomeController’, [‘$scope’, ‘SomeService’,
function($scope, SomeService) {
var listener = SomeService.on(‘update’, function(data) { $scope.data = data; }); $scope.$on(‘$destroy’, function() { SomeService.off(listener); });}]);

The above approach works very well if you like a more explicit way of listening to services. The approach however does not work very well when listening to controllers.

Directive Controllers

The approach we take to listening to controllers came about when we figured out why we would listen to controllers. We wanted to listen to controllers specifically when communicating between directives. It was then that we realized that angular already had this functionality built in. They have done a good job of documenting this implementation here.

The concept is that instead of using $broadcast/$emit, you would rely on your controller to update your dependent directives in the fashion of the mediator pattern.

Using these three types of communication in conjunction with angular’s built-in two-directional bindings should decouple and clean up most cases where you would otherwise have messy events and watches flying around in all directions. For a higher level view of keeping your applications scalable, I suggest reading through this link in order to inform yourself of the many ways to design pieces of your application.

--

--