An angular.js event bus with postal.js

javascript Apr 30, 2014

Ideally in an angular application, controllers are independent units of code that have no reference to any other controllers. There are cases though in which you may need to communicate with other controllers in your application. For example you may have an Orders controller, that needs to tell a Cart controller that a new item has been added.

One of the best ways of accomplishing this type of communication is through the use of an event bus.

My favorite event bus framework is postal.js.

What is it?

Postal.js is an in-memory message bus - very loosely inspired by AMQP - written in JavaScript. Postal.js runs in the browser, or on the server-side using Node.js. It takes the familiar "eventing-style" paradigm (of which most JavaScript developers are familiar) and extends it by providing "broker" and subscriber implementations which are more sophisticated than what you typically find in simple event delegation.

You can use postal by publishing messages accross specific channels. In this way you can segregate your messages to things like app, cart, ui, etc. It also uses an envelope pattern to prevent having n arguments in your subscription callbacks.

Using postal.js, you can easily decorate the $scope and add a $bus that will allow you to communicate with other controllers in your application.

angular.module('myApp')
    .config(function ($provide) {
        $provide.decorator('$rootScope', [
        	'$delegate', function ($delegate) {
            Object.defineProperty($delegate.constructor.prototype, 
            '$bus', {
                value: postal,
                enumerable: false
            });

            return $delegate;
        }]);
    });

Now in your controllers you have postal available as $scope.$bus...

'use strict';

angular.module('myApp')
    .controller('CartCtrl', ['$scope', function ($scope) {
        $scope.$bus.subscribe({
            channel: 'orders',
            topic: 'order.new',
            callback: function(data, envelope) {
                console.log('it worked', data, evenlope);
            }
        });
    }
])
	.controller('OrderCtrl', ['$scope', function ($scope) {
        $scope.order = function() {
        	$scope.$bus.publish({
              channel: 'orders',
              topic: 'order.new',
              data: { /* order info */ }
          });
        };
    }
]);

In the CartCtrl, the $scope.$bus.subscribe method is called. This subscription gets set up to listen on the orders channel for any messages with the order.new topic. When a message comes throught that matches that topic, the callback will be invoked.

In the OrderCtrl, the $scope.$bus.publish method is called whenever $scope.order is called. This publishes a message on the right channel, with the right topic that will trigger the subscription. The data sent via the publish will be recieved in the callback of the subscription, as well as an envelope, which is a wrapper around the data.

Conclusion

Using the $bus decorator makes communicating with other controllers extremely simple. Of course you can use any messaging framework you'd like. If you'd like to learn more about postal, check out the repo, clone it, and feel free to give Jim Cowart some love for making such a great library!

UPDATE 5/6/14

A commenter pointed out that there's a potential memory leak since controller instances are created multiple times. I went in and took a look and added some code to listen to the $destroy event on the scope and call unsubscribe.

angular.module('introToAngularApp')
.config(function ($provide) {
    $provide.decorator('$rootScope', ['$delegate', function ($delegate) {
        Object.defineProperty($delegate.constructor.prototype, '$bus', {
            get: function() {
                var self = this;

                return {
                    subscribe: function() {
                        var sub = postal.subscribe.apply(postal, arguments);

                        self.$on('$destroy',
                        function() {
                            sub.unsubscribe();
                        });
                    },
                    channel: postal.channel,
                    publish: postal.publish
                };
            },
            enumerable: false
        });

        return $delegate;
    }]);
});

I had to use the get of Object.defineProperty so that I can have a hold of the correct this for the $on. Then it simply listens for that event, and calls sub.unsubscribe().

That should help alleviate any memory leak problems!

Tags