Angular's Event Bus FTW!

For the past few weeks my friends and I have been working on a mobile web app called Sweatr (yeah, that's right, Sweatr) that allows a user to match up their fitness metrics to other users. Sweatr was built using Ionic's incredible hybrid mobile app framework which itself is built on top of Angular. A little over a week ago I was faced with an interesting technical challenge which I did not have an immediate remedy for. After some research, I discovered that Angular's event bus would be the solution to my problem. This post will provide background on the problem and detail how Angular's event bus solved it.

In the Sweatr app we are using the popular card UI pattern (see below).

Each card represents the fitness data of a user that you want to compare your fitness stats with (as you see, I haven't been very active lately). You will notice that there is an icon in the top right corner of each card. When this icon is tapped, the information on the front of the card will transition away and will be replaced with dynamically generated charts of the last 7 days of two users Fitbit data (see below).

Notice that there are actually three charts (indicated by the 3 dots on the bottom) and that the user is able to swipe left or right to navigate through the charts. The template markup for our cards UI is shown below with details removed for brevity.

cards.html

<div class="list card animate flip-in shadow" ng-repeat="user in users">  
      <figure class="cardFront">
      <!-- Card Header -->
      <div class="item item-avatar item-header-bg">
          <img ng-src={{user.user.authData.fitbit.avatar}} >
          <h2>{{user.user.username}}</h2>
          <div class="col">
            <i class="icon ion-stats-bars card-switch sweatr-bars" 
                ng-show="backShown"
                ng-click="backShown=!backShown;">
            </i>
            <i class="icon ion-arrow-graph-up-right card-switch sweatr-chart" 
                ng-show="!backShown"
                ng-click="backShown=!backShown; onclick();">
            </i>
          </div>
        </div>
      </div>

      <!-- Card FRONT Body -->
      <div class="item item-body animate bouncy-slide-left item-content" 
          ng-if="!backShown">
        <ul class="list">
          ...
        </ul>
      </div>

    <!-- Card BACK Body (chart) -->
    <div ng-controller="ChartController">
      <div class="item item-body chart animate toggle
      ng-if="backShown">
        <slide-box>
          <slide ng-repeat="(statCategory, stat) in statCategories">
            <h3>{{statCategory}}</h3>
            <nvd3-line-chart
              ...
              <svg></svg>
            </nvd3-line-chart>
          </slide>
        </slide-box>
      </div>
    </div>
  </figure>
</div>  

Let me briefly explain what is happening in the markup above. To implement this card UI, each card is a dynamically created via a ng-repeat over a users collection. Within each card, the 3 charts are dynamically created via another ng-repeat over a statsCategories collection.

Now, as I mentioned the charts are dynamically generated. When the icon is clicked this initiates an AJAX call to our API requesting the activity data of the current user and the user that the current user wants to compare themselves to.

The Problem

If you reference the code above you will see that there is a ChartController that is managing the scope that encapsulates the chart views. What you do not see is the CardsController that is managing the the scope of the entire user stream (this is specified within our router logic).

Now, as I mentioned, clicking the chart icon initiates an AJAX call to our API. The logic that manages the AJAX and the creation of data sets for the charts resides in the ChartController (see below).

chartController.js

angular.module('fittr.controllers')

.controller('ChartController', function($q, $scope, $timeout, UserService){

  var stepsDatum = null;
  var milesDatum = null;
  var activeDatum = null;

  // Generation of comparison data
  // ==========================================================================
  var alreadyCalled = false;

  var getWeekly = function(userId) {
    if (alreadyCalled) { return; }

    UserService.getWeekly(userId)
    .then(function(data) {
      stepsDatum = buildChartData(data, 'steps');
      milesDatum = buildChartData(data, 'distance');
      activeDatum = buildChartData(data, 'veryActiveMinutes');

      $scope.statCategories = {
        'Steps':stepsDatum,
        'Miles':milesDatum,
        'Active':activeDatum
      };
      alreadyCalled = true;
    }, function(status) {
      console.log("An error occurred during the call to get" + status);
    });
  };

  ...

});

Unfortunately, since the logic above resides in a child scope (actually grandchild scope) of the CardsController's scope and the chart icon resides in the CardsController scope, I could not figure out a way to let the ChartController know that the chart icon had been clicked. Enter Angular's event bus.

In Angular, scopes organized in a hierarchy can be used as an event bus. What this means is that you can propagate custom events through the hierarchy of scopes. To propagate an event upwards through the hierarchy of scopes you would use the $emit method of the $scope object. To propagate an event in the opposite direction you would use the $broadcast method. An event handler for any of these custom can be registered via the invocation of the scope's $on method.

So, here is how I solved the problem of having my chartController respond to an action taken in a scope higher up in the hierarchy of its scopes.

In the cards template, I specify that the onclick method will be invoked whenever the chart icon is clicked (via the ng-click directive)

cards.html

<div class="col">  
    <i class="icon ion-stats-bars 
        card-switch sweatr-bars" 
        ng-show="backShown"
        ng-click="backShown=!backShown; onclick();">
    </i>
    <i class="icon ion-arrow-graph-up-right 
        card-switch sweatr-chart" ng-show="!backShown"
        ng-click="backShown=!backShown; onclick();">
    </i>
</div>  

In the CardsController the onclick method invokes the scope's $broadcast method which triggers a custom chartButtonClick event downward through the scope hierarchy.

cardsController.js

angular.module('fittr.controllers')  
    .controller('CardsController', function($q, $scope, $timeout, UserService){
  ...
  // broadcast to the child chartControllers that the chart button in the card's
  // header has been clicked
  $scope.onclick = function() {
    this.$broadcast('chartButtonClick');
  };
});

In the chartController I've registered an event handler for the custom chartButtonClick event, in which I invoke the getWeekly function (if you remember, getWeekly manages the retrieval of fitness data).

chartController.js

$scope.$on('chartButtonClick', function() {
    getWeekly(($scope.user.user._id));
  });

With these changes in place, my problem has been solved. Now, when a user taps on the chart icon, the getWeekly method within the corresponding ChartController is called.

I know this was a long post, and thanks for hanging in there. Hopefully you learned a bit about Angular's event bus.

/content/images/2016/01/twitter_pic_400x400.jpeg

Front-End Engineer with a passion for beautiful UI, data visualization and interaction design. Oakland, CA is the place I call home.

Latest posts

Recent Tweets