AngularJS, code.explode, Coding

Hailing All Frequencies – Communicating in AngularJS with the Pub/Sub Design Pattern

Hailing All Frequencies! Is anybody out there? This is the USS AngularJS, we have a problem our services are speaking in Klingon and our controllers can’t communicate with their Ferengi directives. Can anyone help us!

I don’t know how many times I’ve seen a question about what is the best way to communicate between components in AngularJS. A lot of the time the resulting answer is to use the $rootScope object to $broadcast a message to anyone who might be listening for it. But, that really isn’t the best way to do it. Broadcasting messages between components means that they need to know too much about how things are coded reducing their modularity and re-use.

In this article I show you how to use the Pub/Sub Design Pattern for inter-component communications in AngularJS.

AngularJS has several ways that you can use to communicate between components, but the most used methods require your components to know too much about those they are communicating with, which increases the coupling between components and decreases their modularity and cohesion. This makes it hard to reuse your components in other applications.

By using the Publish/Subscribe design pattern we can decrease the coupling between components and encapsulate the details used for component communications. This will help increase your component’s modularity, testability and reuse.

The implementation of the Publish/Subscribe pattern I’ll be covering was devised by Eric Burley, @eburley, in his post angularjs.org watch, on pub sub, and you..

The example application, I’ll be describing, shows how you can use the Publish/Subscribe pattern for inter-controller communications and controller to service communications. You can find the source code out on GitHub under my repository angularjs-pubsub.

First We Need a Channel to Communicate Through

First let’s talk about the service that is used by the publishers and subscribers to communicate with each other. I’ve defined a service that will provide an interface that has a publish and subscribe method for each message we want to send.

In the code below I have defined two internal messages; _EDIT_DATA_, used to indicate that we need to edit the item passed in the message and _DATA_UPDATED_, used to indicate that our data has changed. Since these are defined internally to our service none of the consumers of the service will have access to them, helping to keep the implementation details hidden.

Each message in turn, has two methods; one to publish the message to the subscribers and another one that the subscribers will user to register a callback method that will be called when the message is received.

The methods to publish the messages to the subscribers are editData, on line 9, and dataUpated, on line 19. They use the $rootScope.$broadcast method to publish the private notification message to the event handlers.

The event registration methods, use the $scope.$on methods to set up a watch that will get called whenever the message is broadcast and in turn call the event handler that was passed when the subscriber registered with the service. Since the subscriber also passes in its own scope, we can use it to perform the watch on the message and avoid the heavy code needed to manage a list of listeners. The registration methods are onEditData, on line 13 and onDataUpdated on line 23.

To hide the implementation details, I’ve used a Revealing Module Pattern to only return back the methods that I want to be accessible by the consumers.

angular.module(['application.services'])
    // define the request notification channel for the pub/sub service
    .factory('requestNotificationChannel', ['$rootScope', function ($rootScope) {
        // private notification messages
        var _EDIT_DATA_ = '_EDIT_DATA_';
        var _DATA_UPDATED_ = '_DATA_UPDATED_';

        // publish edit data notification
        var editData = function (item) {
            $rootScope.$broadcast(_EDIT_DATA_, {item: item});
        };
        //subscribe to edit data notification
        var onEditData = function($scope, handler) {
            $scope.$on(_EDIT_DATA_, function(event, args) {
               handler(args.item);
            });
        };
        // publish data changed notification
        var dataUpdated = function () {
            $rootScope.$broadcast(_DATA_UPDATED_);
        };
        // subscribe to data changed notification
        var onDataUpdated = function ($scope, handler) {
            $scope.$on(_DATA_UPDATED_, function (event) {
                handler();
            });
        };
        // return the publicly accessible methods
        return {
            editData: editData,
            onEditData: onEditData,
            dataUpdated: dataUpdated,
            onDataUpdated: onDataUpdated
        };
    }])

Publishing a Message

To publish a message is pretty simple, first we need to include a dependency to the requestNotificationChannel in our controller, service or directive. You can see this in line 2 in the dataService definition below. When an event occurs in the component that needs to signal others that a change has occurred, you just need to call the appropriate notification method on the requestNotificationChannel. If you look at the saveHop, deleteHop and addHop methods of the dataService, you’ll see they all call the dataUpdated method on the requestNotificationChannel which will then signal the listeners who have registered with the onDataUpdated method.

    // define the data service that manages the data
    .factory('dataService', ['requestNotificationChannel', function (requestNotificationChannel) {
        // private data
        var hops = [
            { "_id": { "$oid": "50ae677361d118e3646d7d6c"}, "Name": "Admiral", "Origin": "United Kingdom", "Alpha": 14.75, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Bittering hops derived from Wye Challenger.  Good high-alpha bittering hops. Use for: Ales Aroma: Primarily for bittering Substitutions: Target, Northdown, Challenger", "Type": "Bittering", "Form": "Pellet", "Beta": 5.6, "HSI": 15.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} ,
            { "_id": { "$oid": "50ae677361d118e3646d7d6d"}, "Name": "Ahtanum", "Origin": "U.S.", "Alpha": 6.0, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Distinctive aromatic hops with moderate bittering power from Washington. Use for: Distinctive aroma Substitutes: N/A", "Type": "Aroma", "Form": "Pellet", "Beta": 5.25, "HSI": 30.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} ,
            { "_id": { "$oid": "50ae677361d118e3646d7d6e"}, "Name": "Amarillo Gold", "Origin": "U.S.", "Alpha": 8.5, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Unknown origin, but character similar to Cascade. Use for: IPAs, Ales Aroma: Citrus, Flowery Substitutions: Cascade, Centennial", "Type": "Aroma", "Form": "Pellet", "Beta": 6.0, "HSI": 25.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} ,
            { "_id": { "$oid": "50ae677361d118e3646d7d6f"}, "Name": "Aquila", "Origin": "U.S.", "Alpha": 6.5, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": "Aroma hops developed in 1988.  Limited use due to high cohumolone.Used for: Aroma hops Substitutes: ClusterNo longer commercially grown.", "Type": "Aroma", "Form": "Pellet", "Beta": 3.0, "HSI": 35.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} ,
            { "_id": { "$oid": "50ae677361d118e3646d7d70"}, "Name": "Auscha (Saaz)", "Origin": "Czech Republic", "Alpha": 3.3, "Amount": 0.0, "Use": "Boil", "Time": 0.0, "Notes": " Use for: Pilsners and Bohemian style lagers Aroma: Delicate, mild, clean, somewhat floral -- Noble hops Substitute: Tettnanger, LublinExamples: Pulsner Urquell", "Type": "Aroma", "Form": "Pellet", "Beta": 3.5, "HSI": 42.0, "Humulene": 0.0, "Caryophyllene": 0.0, "Cohumulone": 0.0, "Myrcene": 0.0, "Substitutes": ""} ,
        ];
        // sends notification that data has been updated
        var saveHop = function(hop) {
            requestNotificationChannel.dataUpdated();
        };
        // removes the item from the array and sends a notification that data has been updated
        var deleteHop = function(hop) {
            for(var i = 0; i < hops.length; i++) {
                if(hops[i]._id.$oid === hop._id.$oid) {
                    hops.splice(i, 1);
                    requestNotificationChannel.dataUpdated();
                    return;
                }
            };
        };
        // internal function to generate a random number guid generation
        var S4 = function() {
            return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
        };
        // generates a guid for adding items to array
        var guid = function () {
          return (S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase();
        };
        // function to add a hop to the array and sends a notification that data has been updated
        var addHop = function(hop) {
            hops.id.$oid = guid();
            hops.push(hop);
            requestNotificationChannel.dataUpdated();
        };
        // returns the array of hops
        var getHops = function() {
            return hops;
        };
        // returns a specific hop with the given id
        var getHop = function(id) {
            for(var i = 0; i < hops.length; i++) {
                if(hops[i]._id.$oid === id) {
                    return hops[i];
                }
            };
        };
        // return the publicly accessible methods
        return {
            getHops: getHops,
            getHop: getHop,
            saveHop: saveHop,
            deleteHop: deleteHop,
            addHop: addHop
        }
    }]);

Receiving Notification Events

Receiving event notifications from the requestNotificationChannel is just as simple, except we need to pass in a callBack handler that will do something with the notification once the message is sent. Again we will need to add a dependency to the requestNotificationChannel to our controller, service or directive, you can see this on line 2 of the code for the view1-controller below. Next we need to define an event callback handler that will respond to the event notification, you can see this on line 5 below. Next we need to register our callback handler with the requestNotificationChannel by calling onDataUpdated and passing in the scope from our controller and the callback handler, we do this on line 9 below.

    //define the controller for view1
    .controller('view1-controller', ['$scope', 'dataService', 'requestNotificationChannel', function($scope, dataService, requestNotificationChannel) {
        $scope.hops = dataService.getHops();

        var onDataUpdatedHandler = function() {
            $scope.hops = dataService.getHops();
        }

        requestNotificationChannel.onDataUpdated($scope, onDataUpdatedHandler);

        $scope.onEdit = function(hop) {
            requestNotificationChannel.editData(hop);
        }

        $scope.onDelete = function(hop) {
            dataService.deleteHop(hop);
        }
    }]);

Controller to Controller Communication

We can use the requestNotificationChannel to communicate between controllers as well. We just need to have one controller act as a publisher and one as a subscriber. If you look at the onEdit method of the view1-controller on line 11 above, it sends an editData message with the item that needs to be edited using the requestNotificationChannel. The view2-controller below registers its onEditDataHandler with the requestNotificationChannel on lines 5 thru 9 below. So whenever the view1-controller sends the editData message with the item to edit, the view2-controller gets notified of the editData request and takes the item received and assigns it to its model.

    //define the controller for view1
    .controller('view2-controller', ['$scope', 'dataService', 'requestNotificationChannel', function($scope, dataService, requestNotificationChannel) {
        $scope.hop = null;

        var onEditDataHandler = function(item) {
            $scope.hop = item;
        };

        requestNotificationChannel.onEditData($scope, onEditDataHandler);

        $scope.onSave = function() {
            dataService.saveHop($scope.hop);
            $scope.hop = null;
        }

        $scope.onCancel = function() {
            $scope.hop = null;
        }
    }]);

Always Have Well Documented Interfaces

One of the things that may not be apparent, is that we are using an interface to communicate between components, and with any interface, they need to be well documented in order to be properly used. In the example above, without the right documentation, consumers of the onEditData message would never know that an item is passed to the call back handler. So as you start to use the pattern, its good practice to document the methods so those consuming your notification services know what to expect.

The Wrap Up

So I've covered how to use the Publish/Subscribe pattern for component communications in your AngularJS App. The pattern allows you to decouple your components from internal messages and promotes better reuse. You can take using the Publish/Subscribe pattern even farther by replacing all communication between your components. This pattern works especially well when you have a lot of asynchronous requests in your services and want to cache the data in your service to reduce communications with the server side.

I hope you find this helpful, as always you can find the code out on my GitHub site under my repository angularjs-pubsub.

Standard
Loading Facebook Comments ...

Got a comment? I'd love to hear it!