Your Backbone Can't Save You Now: Why We Use Backbone Marionette

The big ball of mud is a well understood software anti-pattern, and we’ve all experienced it at some point in our careers (sometimes on the receiving end but all too often as the creator). There is never a specific event where we can look back and say, “this is what caused the problems we’re experiencing right now.” It’s cumulative, subtle, and it takes time to manifest.

But how does this happen? We’re agile. We pair program for instant code reviews. We follow the YAGNI principle and only write the functionality we need. And yet, we can often end up with code that is difficult to understand, change, and test. New features take much longer to add because we don’t understand the code we’re working on and any change we make breaks another part of the code base.

The Situation

We have an input form to let a user create a new ad creative. It’s sufficiently complicated because a creative has a lot of moving parts, some required, some optional, and the user needs to be able to edit any of those parts. There is the content of the creative, information about the targeting, and the budget information. We also want to show the user what the ad unit will look like as the user enters information. It ain’t trivial.

We use backbone.js to organize our JavaScript. It gives us powerful ways to organize our code, but it doesn’t provide everything we need in a framework. True, it’s easy to shoot yourself in the foot with any code, but Backbone’s lack of opinion makes it easier. The documentation says that it “is an attempt to discover the minimal set of data-structuring (models and collections) and user interface (views and URLs) primitives that are generally useful when building web applications with JavaScript.” Developers have the freedom to use Backbone as they wish, but it’s not a fully-featured framework.

It turns out that we have a bit too much freedom and now we have some major concerns in our code.

Structure

Our back end code is written in Ruby on Rails. Rails has a specific file structure based on its role (eg, controllers, models, or views) and uses “magic” to autoload the files. This design decision makes sense for Rails, but it doesn’t make sense in our situation. Take a look:

1
2
3
4
backbone/
  helpers/
  routers/
  views/

These folders don’t give us a lot of information about the domain of what we’re trying to model. It does tell us what types of files we have, but this is not very helpful.

DOM Spaghetti

Backbone views are the heart of an application. The documentation states, “The general idea is to organize your interface into logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page.” There is some structure, but it’s easy to make views junk drawers that are organized haphazardly. A critique of jQuery is that it’s easy to create a lot of selectors that are difficult to organize, and Backbone.js views can easily consist of this mess instead of breaking it up into organized units.

Testing Is Hard

We test our JavaScript with Jasmine, and the setup required for some of our tests is intimidating. There are tests that recreate a large part of the DOM in fixtures, use a lot of selectors to grab various bits, and then do logic on multiple pieces at once.

This coupling makes changing anything very difficult since the change can affect so many different parts. Difficult tests are no fun for anyone.

Won’t Someone Do Something!

With all this, we were still putting out new features, although they were small and easily understood. We recently planned work for a much larger feature that would make our creatives much more powerful. After evaluating what this feature requires, we realized that it would be very difficult to add to the code base in its existing form. We decided we needed to give our code more structure.

Backbone Marionette: Our New Puppet Master

Derick Bailey explains his reasoning for using Marionette, and it’s compelling. Here are his reasons and what we think of them:

Composite application architecture

We get much more value out of our code when we prefer composition over inheritance.

Enterprise messaging pattern influence

The pub/sub pattern gives us loose coupling, letting us create multiple smaller components instead of one large, monolithic application.

Modularization options

We don’t use any existing module frameworks so Marionette’s module system works well for us.

Incremental use (no all-or-nothing requirement)

Since we’ve got an existing application, it makes more sense to add or change functionality in pieces instead of all at once.

No server lock-in

Our backend is Rails without any plans to change it, so this isn’t too relevant except that it allows us to use our preferred back-end technology.

Easily change the defaults

All applications seem to need more than what a framework provides by default, so making this easy makes developers happy.

Code as configuration

This goes with the previous reason. The frameworks author’s conventions probably overlap a lot with ours, but it won’t be 100%.

A Before/After

Let’s look at an example before and after. We have all the logic of the form for a new creative in one view.

  • We set up some event handlers
  • have the logic to fetch the OpenGraph tags for a URL through an Ajax request and populate the data in the form based on the response
  • We do all this without any models, just jQuery selectors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Bakery.Views.CreativeContentForm extends Backbone.View
  initialize: ->
    @setupClickHandler()

  setupClickHandler: () ->
    $('#fetch').click (e) =>
      e.preventDefault()
      url = $('#creative_content_url').val()
      $('#error-container').hide()
      $('#loading-spinner').fadeIn()
      $.ajax({
        url: '/creatives/fetch_open_graph_data'
        type: 'GET'
        data: {url: url}
        success: (data) =>
          @loadDemoContent(data)
        error: =>
          @showError()
        dataType: 'json'
      })

  loadDemoContent: (data) ->
    $('#creative_headline').val(data.title).trigger('keyup')
    if(data.description && data.description.length > 1)
      $('#creative_description').val(data.description).trigger('keyup')
    else
      $('#creative_description').val(data.title).trigger('keyup')
    $('#creative_thumbnail_url').val(data.image)

    if data.url? && data.url.match(YOUTUBE_ID_REGEX)?
      @setAsYoutube(data)
    else if data.twplayer_stream? && $('#creative_content_url').val().match(VINE_REGEX)?
      @setAsVine(data)
    else
      @setAsClickout(data)

    @updateDemo()

  setAsYoutube: (data) ->
    $('#creative_media_url').val($('#creative_content_url').val())
    $('#creative_thumbnail_url').val('http://i1.ytimg.com/vi/' + data.url.match(YOUTUBE_ID_REGEX)[1] + '/mqdefault.jpg')

  setAsVine: (data) ->
    $('#creative_media_url').val(data.twplayer_stream)

  setAsClickout: (data) ->
    $('#creative_media_url').val($('#creative_content_url').val())

  showError: () ->
    $('#loading-spinner').fadeOut()
    $('#error-container').fadeIn()
    $('#creative_headline').val('')
    $('#creative_description').val('')
    $('#creative_thumbnail_url').val('')

  updateDemo: () ->
    $('#error-container').hide()
    if($('#creative_thumbnail_url').val().length > 0)
      thumbnailUrl = $('#creative_thumbnail_url').val()
      $('#preview-thumbnail').css('background-image',  "url(#{thumbnailUrl})")
    $('#preview-title').html($('#creative_headline').val().substring(0, TITLE_CHAR_LIMIT))
    $('#preview-description').html($('#creative_description').val().substring(0, DESCRIPTION_CHAR_LIMIT))
    $('#loading-spinner').fadeOut()

We’ll leave it as an exercise to the reader to imagine the tests for this.

Now let’s look at what we have with Marionette.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# in the Rails view
App.CreativeDetailsFetcher.start(model: creative)

# creative_details_fetcher_app.js.coffee
CreativeDetailsFetcher.on 'start', (options) ->
  App.addRegions
    creativeDetailsFetcherRegion: '#js-creative-details-fetcher'

  API.listCreativeDetailsFetcher(options.model)

# list_controller.js.coffee
List.Controller =
  listCreativeDetailsFetcher: (creative) ->
    view = new List.DetailsFetcher(model: creative)
    App.creativeDetailsFetcherRegion.show(view)

# list_view.js.coffee
class List.DetailsFetcher extends Marionette.ItemView
  template: 'creative_details_fetcher/list/templates/details_fetcher'

  events:
    'click #fetch': 'fetchData'
    'keypress': 'handleKeyPress'

  ui:
    content_url: 'input[type="text"]'
    spinner: '#url_loading_spinner'

  handleKeyPress: (e) ->
    enterCharCode = 13
    if e.charCode == enterCharCode
      @fetchData()

  onShow: ->
    @ui.content_url.focus()

  fetchData: ->
    @model.fetch(
      data: { url: @model.get('content_url') }
      success: @fetchDataHandler
      error: @fetchDataHandler
    )
    @ui.spinner.fadeIn()

  fetchDataHandler: (model, response, options) =>
    @publishType(options.creativeType)

  publishType: (type) ->
    List.trigger('select:type', type, @model)

This is a few steps in the right direction (and heavily redacted). We’ve split out the functionality to a separate that only deals with fetching the data and looks at just the URL input field. Note most importantly that we’ve reduced much of the complexity in the code. Less branching (only one if statement), less jQuery selectors, the view only worries about one focused bit of functionality.

Next, we’ll go into more detail about what makes up a component and how all the pieces fit together.