Hashrocket.com / blog

Bg default article large

Dabbling with Backbone.js

posted on and written by Kevin Wang in

Image 100x100 bb1ab55dc8c1931091dd2aadc9058475

Here at Hashrocket, every Friday afternoon is reserved for contributing to open source or learning new skills. Last Friday I decided to use this time to teach myself Backbone.js, with the goal of building a toy app with it in a few hours.

The idea is a simple "leaderboard" to rank Rocketeers by the number of "hugs" they received on Twitter - to "hug" a rocketeer, you just add the hashtag "#hugarocketeer" on a tweet mentioning a rocketeer's Twitter handle. (You can see the implemented app here.) The main reason for this idea is that I could lean on Twitter APIs to provide data so I can focus on just the front end with Backbone. I have never done a Backbone app before, and I didn't feel like starting from scratch. I grabbed the Backbone implementation of ToDo.js to serve as a starting point as well as to learn from the code base.

First I mocked up the UI with plain HTML and CSS, just to have an idea what the end product will be. The ToDo.js serves as a great base here with all the supporting files and boilerplates set up, so I only had to change the "body" part of the index.html, and modified the css to make it look right.

<body>
  <div id="hug_a_rocketeer">
    <ul id="rocketeers">
      <li>
    <div id='pic_and_name'>
      <img src='http://dummyimage.com/50x50/574b57/bcbfeb.png'/>
      <span> Marian Phalen </span>
    </div>
    <ul id="props">
      <li> 3 Hugs </li>
    </ul>
      </li>
      <li>
    <div id='pic_and_name'>
      <img src='http://dummyimage.com/50x50/574b57/bcbfeb.png'/>
      <span> Micah Cooper </span>
    </div>
    <ul id="props">
      <li> 4 Hugs </li>
    </ul>
      </li>
    </ul>
  </div>
</body>

Extracting Underscore template from the markup:

<body>
  <div id="hug_a_rocketeer">
    <h1> Hug a Rocketeer! </h1>
    <span> Add #hugarocketeer to your tweet mentiong your favorite rocketeer, and watch them climb up on this board. :)
    <ul id="rocketeers">
    </ul>
  </div>

  <script type="text/template" id="person-template">
    <div id='pic_and_name'>
      <img src="<%= img_url %>"/>
      <span> <%= name %> </span>
    </div>
    <ul id="hugs">
      <li> <%= hugs %> hugs </li>
    </ul>
  </script>
</body>

On to the javascript file: ToDo.js has a nice and small Backbone implementation and with helpful comments - I learned the fundamentals of Backbone just by reading the code. For our purpose, I kept the code structure and removed all its logic, and put in our own Backbone model, collection and view.

$(function(){
  var Person = Backbone.Model.extend({
    defaults: {
      hugs: 0
    }
  });

  function retrieveTwitterInfo(element) { 
    {
      img_url: 'https://api.twitter.com/1/users/profile_image?screen_name=' + element + '&size=bigger'
    }
  };

  var rocketeerTwitterHandles = ['marianphalen', 'mrmicahcooper', 'knwang'];
  var rocketeers = rocketeerTwitterHandles.map(retrieveTwitterInfo);


  var People = Backbone.Collection.extend({
    model: Person
  });

  var PersonView = Backbone.View.extend({
    tagName:  "li",

    template: _.template($('#person-template').html()),

    initialize: function() {
      _.bindAll(this, 'render');
      this.model.bind('change', this.render);

    },
    render: function() {
      $(this.el).html(this.template(this.model.toJSON()));
      return this;
    }
  });

  var AppView = Backbone.View.extend({

    el: $("hug_a_rocketeer"),

    initialize: function() {
    },
    render: function(){
      var self = this;
      _(this.collection.models).each(function(item){
    self.appendItem(item);
      }, this);

    },
    appendItem: function(item) {
      var itemView = new PersonView({
    model: item
      });
      $("ul#rocketeers").append(itemView.render().el);
    }
  });

  var App = new AppView;
});

There's no magic at this point, still just to render out a static page, but we are now running this through the Backbone framework. The AppView render() function loops through all the people and append each person's rendered html to "ul#rocketeers". We are, however, pulling off people's profile photo from directly from Twitter with their Twitter handle.

The next iteration took me a while. I wanted to actually hit Twitter's search API for each person to fetch mentions with the hashtag of "#hugarocketeer", and refresh the board on the callbacks. There's a bit of "gotcha" here: though Backbone supports automatically sorting of models in the collections, it happens at the time models are inserted into the collection. In our case, the retrieval of data from Twitter is asynchronous and the "hugs" attribute on Person won't get set until after they are inserted into the collection. So we cannot just rely on Backbone's collection sorting (by defining "comparator" on the collection.) The solution I came up with was to clear the board and sort and re-render on every callback. The result is that you'll see the list flickering a few times until finally settling on the final rank.

$(function(){
  var Person = Backbone.Model.extend({
    defaults: {
      img_url: '',
      name: '',
      hugs: 0,
    },
    refresh_hugs: function() {
      var self = this;
      $.getJSON(this.get("search_url"), function(data) {
    var hugs = data.results.length;
    self.set({hugs: hugs});
      })
    }
  });

  var PersonView = Backbone.View.extend({
    tagName:  "li",
    template: _.template($('#person-template').html()),
    initialize: function() {
      _.bindAll(this, 'render');
      this.model.bind('change', this.render);
    },
    render: function() {
      $(this.el).html(this.template(this.model.toJSON()));
      return this;
    }
  });

  var rocketeers = ['marianphelan', 'mrmicahcooper', 'p_elliott', 'knwang', 'martinisoft', 'adam_lowe', 'bthesorceror', 'higgaion', 'camerondaigle', 'ChrisCardello', 'biggunsdesign', 'daveisonthego', 'jonallured', 'joshuadavey', 'videokaz', 'mattonrails', 'mattpolito', 'therubymug', 'shaneriley', 'shayarnett', 'voxdolo', 'syntaxritual', 'johnny_rugger'].map(function(twitter_handle){
    return new Person({
      img_url: "https://api.twitter.com/1/users/profile_image?screen_name=" + twitter_handle  + "&size=normal",
      name: '@'+ twitter_handle,
      search_url: 'http://search.twitter.com/search.json?q=' + '@'+ twitter_handle + '%20' + "%23hugarocketeer" + "&rpp=100&callback=?"
    });
  });

  var People = Backbone.Collection.extend({
    model: Person,

    comparator: function(person) {
      return -person.get('hugs');
    }
  });

  var AppView = Backbone.View.extend({

    el: $("#hug_a_rocketeer"),

    returned_results: 0,

    initialize: function() {
      var self = this;
      this.collection = new People();

      _.each(rocketeers, function(person) {
    $.getJSON(person.get("search_url"), function(data) {
      var hugs = data.results.length;
      person.set({hugs: hugs});
      self.collection.add(person);
      self.returned_results += 1;
      if (self.returned_results == rocketeers.length) {
        self.render();
      }
    })
      });
      _.bindAll(this, 'render');
    },

    render: function(){
      var self = this;
      _(this.collection.models).each(function(item){
    self.appendItem(item);
      }, this);
    },

    appendItem: function(item) {
      var itemView = new PersonView({
    model: item
      });
      $("ul#rocketeers").append(itemView.render().el);
    },

  });

  var App = new AppView;
});

This is actually when I stopped last week after about 5 hours of hacking... not bad to pick up a new technology and make a fun app to entertain my coworkers.

Just as I was finishing this blog post, I asked @shaneriley, our Javascript expert-in-residence, for the asynchronous callback ordering problem, and he suggested a better solution - I could count the number of returned callbacks from Twitter queries and render just once once I know all have come back. So here it is for that:

....

returned_results: 0,

initialize: function() {
  var self = this;
  this.collection = new People();

  _.each(rocketeers, function(person) {
    $.getJSON(person.get("search_url"), function(data) {
      var hugs = data.results.length;
      person.set({hugs: hugs});
      self.collection.add(person);
      self.returned_results += 1;
      if (self.returned_results == rocketeers.length) {
        self.render();
      }
    })
  });
  _.bindAll(this, 'render');
},

....

Check it out live and start hugging rocketeers!

Code is here on github

Posted in Development and tagged with Javascript