Refactoring to Backbone.js

Nate Hunzaker, Former Development Director

Article Category: #Code

Posted on

Deciding mid-stride to adopt a JavaScript application framework exposes many difficult decisions. What should be targeted first? How does one deal with untested, legacy code? It can make transitions a scary, tedious process.

At Viget, we've found Backbone to be a great tool for bolstering existing code with greater structure and functionality. With an active community and a number of superset frameworks (such as Thorax and Marionette), it gives us flexible tools to tackle current problems with an outlet for more elegant solutions down the road.

We've started to trend towards a series of steps that help to translate code over in a safer, more testable way.

Let's start by considering the following code:

function News(options) {
  this.$container = $(options.container)
  this.$element = $('<ul>', { class: 'news-widget' })
}

News.prototype = {
  addEntry: function(data) {
    var entry = $('<li>'),
      date = new Date(data.date)

    var prettyDate = [date.getMonth(), date.getDate(), date.getFullYear()].join(
      '/'
    )

    entry.append('<h3>' + data.title + '</h3>')
    entry.append('<p>' + prettyDate + '<p>')
    entry.append('<p>' + data.summary + '<p>')

    this.$element.append(entry)
  },

  getNews: function() {
    var url = '/my/api/news/',
      request = $.getJSON(url),
      news = this

    request.done(function(data) {
      $.each(data, function(story) {
        news.addEntry(story)
      })
    })
  }
}

This sort of code isn't terrible, but I've intentionally inserted a couple of problems into it that we'll address during our refactor.

Pull HTML construction into a template

Before anything else, JavaScript HTML construction should be moved into a template. I like to do this first because it clears the way for us to focus on a much harder problem: breaking out data processing into template helpers and methods of Backbone.Models and Backbone.Collections.

Let's rewrite that addEntry method, by making a JavaScript template. On most projects we let the Ruby on Rails Asset Pipeline manage our templates. We'll use Handlebars for this example. I love working with Handlebars because it has a great API for writing template helpers and the handlebars-asset gem does a lot of work to setup templates behind the scenes.

Assuming I've added the handlebars-assets gem to my Gemfile, this lets me include templates like so:

{{! templates/news_item }}
<li class="news_item">
  <h3 class="news_item_title">{{ title }}</h3>
  <p class="news_item_meta">{{ prettyDate }}</p>
  <p class="news_item_summary">{{ summary }}</p>
</li>
//= require templates/news_item

News.prototype = {
  template: HandlebarsTemplates['news_item'],
  //...
  addEntry: function(data) {
    var date = data.date

    data.prettyDate = [
      date.getMonth(),
      date.getDate(),
      date.getFullYear()
    ].join('/')

    var entry = this.template(data)

    this.$element.append(entry)
  }
  //...
}

Cool, that fixed that problem. But why does addEntry calculate prettyDate? This should either go into a template helper or a method of a Backbone.Model. Since we may want to use this transformation somewhere else in the application, let's make a Handlebars helper that produces the same result.

Handlebars.registerHelper('prettyDate', function(dateString) {
  var date = new Date(dateString)
  return [date.getMonth(), date.getDate(), date.getFullYear()].join('/')
})
{{! templates/news_item }}
<li class="news_item">
  <h3 class="news_item_title">{{ title }}</h3>
  <p class="news_item_meta">{{prettyDate date}}</p>
  <p class="news_item_summary">{{ summary }}</p>
</li>
News.prototype = {
  //...
  addEntry: function(data) {
    var entry = this.template(data)
    this.$element.append(entry)
  }
  //...
}

Cool, no? Moving data transformation into template helpers is one of my favorite parts of refactoring. Over the course of a project, templates grow into powerful tools that can significantly reduce repetition.

We can go a step further, though. Instead of appending a bunch of strings to the end of the list of news items, we can delegate it to Handlebars. This is a perfect use case for partial templates.

Let's put an underscore in front of the file name for our item template, so that handlebars-assets knows to register it as a partial. Then we'll make template that can iterate over a list of items.

{{! templates/_news_item }}
<li class="news_item">
  <h3 class="news_item_title">{{ title }}</h3>
  <p class="news_item_meta">{{prettyDate date}}</p>
  <p class="news_item_summary">{{ summary }}</p>
</li>
{{! templates/news_list}}
{{#each items}}
  {{> _news_item}}
{{/each}}
//= require templates/_news_item
//= require templates/news_list

News.prototype = {
  template: HandlebarsTemplates['news_list'],
  itemTemplate: HandlebarsTemplates['_news_item'],

  render: function(data) {
    var markup = this.template({ items: data })
    this.$element.html(markup)
  },

  addEntry: function(item) {
    var markup = this.itemTemplate(item)
    this.$element.append(markup)
  },

  getNews: function() {
    var url = '/my/api/news/',
      request = $.getJSON(url),
      news = this

    request.done(function(data) {
      news.render(data)
    })
  }
}

When the News object gets fresh data from the server it will now invoke the render method. This gives us better control over output when dealing with changes in data later on. By using a template partial, News supports the ability to iterate over items with a "list" template while still having enough control over markup to append an additional item if need be.

With a strong foundation built for HTML construction it's time to focus on data. In the next steps we'll draw greater focus on how information flows, and how to handle when things change.

Break out Data Processing

Now that construction of the markup has been tucked away into a template, we can focus building the data layer for our News object. In this step, we'll pull all data handling into a Backbone.Model and Backbone.Collection; hooking into the events Backbone provides to support the existing functionality.

var NewsModel = Backbone.Model.extend({
  urlRoot: '/my/api/news'
})

var NewsCollection = Backbone.Collection.extend({
  model: NewsModel,
  url: '/my/api/news'
})

The Backbone.Model and Backbone.Collection can already handle fetching data from the server using the fetch method, so the getNews method we saw earlier can be completely eliminated from News. We'll see how that fits together in the next step.

Finishing things up with Backbone.Views

All that's left is to go full circle, by converting what we have left into a Backbone.View. Usually at this point, the final step is very simple. Our final Backbone.View looks like:

//= require templates/_news_item
//= require templates/news_list

var NewsView = Backbone.View.extend({
  tagName: 'ul',
  className: 'news_widget',

  template: HandlebarsTemplates['news_list'],
  itemTemplate: HandlebarsTemplates['_news_item'],

  initialize: function(options) {
    this.listenTo(this.collection, {
      reset: this.render,
      add: this.addEntry
    })
  },

  render: function() {
    var markup = this.template({
      items: this.collection.toJSON()
    })

    this.$el.html(markup)
  },

  addEntry: function(model) {
    var markup = this.itemTemplate(model.toJSON())
    this.$el.append(markup)
  }
})

That's it! Now all that is left is to instantiate NewsView and rejoice

var news = new NewsView({
  collection: new NewsCollection()
})

news.$el.appendTo('.news-container')
news.collection.fetch()

The example we looked at in this post dealt with code that was very straightforward to refactor. However we've used this technique in the past to effectively rework highly complicated code. By focusing on one task at a time and moving in a natural progression, this technique lets us iterate more quickly, seeing the results of reorganization much sooner.

See a gist of the final result

Related Articles