Go back | Day 17

Pushy Angular

Real-time with Angular is a topic that's growing to be an increasingly important in today's fast-moving pace. On Day 9, we looked at how to handle real-time presence with Firebase.

Pusher is especially good for generating real-time data that don't necessarily need custom storage. In this snippet, we're going to build a small dashboard for a server running a tiny stats collection process that runs every 10 seconds.

Servers

The above example is the current load average as well as the current free memory on an instance in ec2. If the above example does not load data within 10 seconds, visit the backend URL to wake it up at http://guarded-peak-8084.herokuapp.com/start

The source for the server process can be found here.

Get pushy

In order to work with the Pusher service, we'll need to sign up for it (obviously). Head to Pusher and sign up for the service. We'll be working with the free account.

Once we've signed up, we'll need to include loading the library in our HTML. Now, we can do this in the usual way by placing a script tag on the page:

<script src="http://js.pusher.com/2.1/pusher.min.js" type="text/javascript"></script>

Or we can create a provider to load the library for us. This has many advantages, the most of which is that it allows us to use Angular's dependency injection with externally loaded scripts.

angular.module('alPusher', [])
.provider('PusherService', function() {
  var _scriptUrl = '//js.pusher.com/2.1/pusher.min.js'
  , _scriptId = 'pusher-sdk'
  , _token = ''
  , _initOptions = {};

  this.setOptions = function(opts) {
    _initOptions = opts || _initOptions;
    return this;
  }

  this.setToken = function(token) {
    _token = token || _token;
    return this;
  }

  // Create a script tag with moment as the source
  // and call our onScriptLoad callback when it
  // has been loaded
  function createScript($document, callback, success) {
    var scriptTag = $document.createElement('script');
    scriptTag.type = 'text/javascript';
    scriptTag.async = true;
    scriptTag.id = _scriptId;
    scriptTag.src = _scriptUrl;
    scriptTag.onreadystatechange = function () {
      if (this.readyState == 'complete') {
        callback();
      }
    }
    // Set the callback to be run
    // after the scriptTag has loaded
    scriptTag.onload = callback;
    // Attach the script tag to the document body
    var s = $document
      .getElementsByTagName('body')[0];
    s.appendChild(scriptTag);
  }

  this.$get = ['$document', '$timeout', '$q', '$rootScope', '$window', 
    function($document, $timeout, $q, $rootScope, $window) {
    var  deferred = $q.defer(),
        socket,
        _pusher;

    function onSuccess() {
     // Executed when the SDK is loaded
     _pusher = new $window.Pusher(_token, _initOptions);
    }

     // Load client in the browser
     // which will get called after the script
     // tag has been loaded
    var onScriptLoad = function(callback) {
      onSuccess();
      $timeout(function() {
        // Resolve the deferred promise
        // as the FB object on the window
        deferred.resolve(_pusher);
      });
    };

    // Kick it off and get Pushing
    createScript($document[0], onScriptLoad);
    return deferred.promise;
   }]
})

This is our preferred method of injecting external libraries as it also makes it incredibly simple to test our external library interactions.

With the PusherService above, we can create a secondary service that will actually handle subscribing to the Pusher events.

We'll create a single method API for the Pusher service that will subscribe us to the Pusher channel. We'll want to make sure that we

angular.module('myApp', ['alPusher'])
.factory('Pusher', function($rootScope, PusherService) {
  return {
    subscribe: function(channel, eventName, cb) {
      PusherService.then(function(pusher) {
        pusher.subscribe(channel)
        .bind(eventName, function(data) {
          if (cb) cb(data);
          $rootScope
            .$broadcast(channel + ':' + eventName, data);
          $rootScope.$digest();
        })
      })
    }
  }
})

This Pusher service allows us to subscribe to a channel and listen for an event. When it receives one, it will $broadcast the event from the $rootScope. If we pass in a callback function, then we'll run the callback function.

For example:

Pusher.subscribe('stats', 'stats', function(data) {
  // from the stats channel with a stats event
});

Tracking nodes

We'll need to keep track of different nodes with our dashboard. Since we're good angular developers and we write tests, we'll store our nodes and their active details in a factory.

There is nothing magical about the NodeFactory and it's pretty simple. It's entire responsibility is to hold on to a list of nodes and their current stats:

angular.module('myApp')
.factory('NodeFactory', function($rootScope) {
  var service = {
    // Keep track of the current nodes
    nodes: {},
    // Add a node with some default data
    // in it if it needs to be added
    addNode: function(node) {
      if (!service.nodes[node]) {
        service.nodes[node] = {
          load5: 0,
          freemem: 0
        };
        // Broadcast the node:added event
        $rootScope.$broadcast('node:added');
      }
    },
    // Add a stat for a specific node
    // on a specific stat
    addStat: function(node, statName, stat) {
      service.addNode(node);
      service.nodes[node][statName] = stat;
    }
  }
  return service;
})

Tracking

We're almost ready to track our server stats now. All we have left to do is configure our Pusher service with our API key and set up our controller to manage the stats.

We need to configure the PusherService in our .config() function, like normal:

angular.module('myApp')
.config(function(PusherServiceProvider) {
  PusherServiceProvider
    .setToken('xxxxxxxxxxxxxxxxxxxx')
    .setOptions({});
})

Now we can simply use our Pusher factory to keep real-time track of our nodes. We'll create a StatsController to keep track of the current stats:

angular.module('myApp')
.controller('StatsController', function($scope, Pusher, NodeFactory) {
  Pusher.subscribe('stats', 'stats', function(data) {
    NodeFactory.addStat(data.node, 'load5', data.load5);
    NodeFactory.addStat(data.node, 'freemem', data.precentfreemem);
  });
  $scope.$on('node:added', function() {
    $scope.nodes = NodeFactory.nodes;
  });
})

Lastly, our HTML is going to be pretty simple. The only tricky part is looping over the current nodes as we iterate over the collection. Angular makes this pretty easy with the ng-repeat directive:

<table>
  <tr ng-repeat="(name, stats) in nodes track by $id(name)">
    <td>{{ name }}</td>
    <td>{{ stats.load5 }}</td>
    <td>{{ stats.freemem }}</td>
  </tr>
</table>

Enjoy this snippet?

Check out our book that's heading to print this week at ng-book.com

The 600+ page book is packed full of Angular content written and designed to get you up to speed with Angular from beginner to expert.

Independently published with content just like what you've just read.

Brought to you by the team behind ng-newsletter