Pimping Backbone with Marionette

September 21, 2014 • hack

Backbone is great for a number of reasons. It is very unopinionated about how you use it, yet it provides the basic set of common constructs every modern Javascript MVC application has: Views, Models and Collections as well as a Router. Unsurprisingly Backbone has a very large community and prevails despite the strong competion of newer JS frameworks. In fact, the clever folks at &yet use it in their very enjoyable “Human Javascript” book and were so convinced that they used it as the basis for their AmpersandJS framework, a library they describe as Backbone 2.0.

We use Backbone for most of our projects because it offers us guidance but leaves us the freedom to only extend the parts we really need. However, this minimalistic approach sometimes leaves a few things to be desired. Some of those pain points that we face on a daily basis are:

  1. Rendering of Collections
  2. Handling of Subviews
  3. Reusability of Modules

But fear not, help is here: Backbone.Marionette. It provides a library that supplements vanilla Backbone very nicely by implementing best practices and common patterns. It has a number of specialized views, introduces a global event and messaging system and helps with modularization and code mixins.

Rendering a Collection

What previously would have required you to cleverly implement a view’s render function can be automated by relying on Marionette’s CollectionView or the CompositeView:

var CompositeView = Backbone.Marionette.CompositeView.extend({
  template : _.template(`
    <table>
      <thead>
        <tr>
          <th>My fancy Collection</th>
        </tr>
      </thead>
      <tbody class='collection-container'></tbody>
    </table>
  ),
  childView : SomeView,
  childViewContainer : ".collection-container"
});

new CompositeView({
  model : someModel,
  collection : someCollection
});

As you can see, a CompositeView allows you to specify a model and a collection. The model will automatically be serialized and can be accessed in your view’s template. The collection however will be rendered into a container element and use the defined childView for every item. This makes rendering collections as lists and tables a breeze. Upon adding or removing items from your collection the view will render itself again.

But what about sorting?

Let’s imagine we need to sort a collection and insert the items in the correct order. It is these little things, where Marionette shines. Plain Backbone already implements sorting on collections by means of the comparator property. Both the CollectionView and the CompositeView respect this order and will render all items accordingly out of the box.

var OrderedCollection = Backbone.Collection.extends({
  comparator : function(a, b) {
    return a < b;
  }
});

new CompositeView({
  collection : new OrderedCollection()
});

Let’s talk about subviews

Subviews need to be rendered into containers, called Regions. Regions can be managed globally with a RegionManager or easier by using a LayoutView. Within your LayoutView you match container selectors and region objects with the regions hash. Showing and hiding a view can then by trigger on the region object itself.

var LayoutView = Backbone.Marionette.LayoutView.extend({
  template : _.template(`
    <h1>MyLayout</h1>
    <div class=".menu"></div>
  `),

  regions : {
    menu: ".menu"
  },

  onRender : function() {
    this.menu.show(new SubView())
  }
});

var layoutView = new LayoutView();
layoutView.render();

Regions are available after calling the render function on a view, since the selector can only be successfully applied after creating the DOM element.

Reusability

Marionette supports structuring of code as Marionette.Behaviors. You can think of them as code Mixins for your Views. Any interaction that doesn’t justify its own extended view can be packaged as a Behavior. How many times did you manage events to close an overlay or toggle a dropdown? These are perfect scenarios for Behaviors. They allow for small and easily testable building blocks for complex interactions. For our example, let’s suppose we have a HTML5 file input that prints a selected file’s path to a textbox.

// Declare a new Behavior
var InputFileBehavior = Marionette.Behavior.extend({
  events : {
    "change [type=file]" : "fileSelectionHandler"
  },

  fileSelectionHandler : function(evt) {
    filePicker = evt.target
    fileNames = _.pluck(filePicker.files, "name").join(" ")

    $inputText = this.$("selected-files")
    $inputText.prop("value", fileNames)
 }
});

// Use the Behavior in a View
var MyView = Marionette.ItemView.extend({
  template : _.template(`
    <form>
      <input type="file">
      <input type="text" id="selected-files">
    </form>
  `),

  behaviors : {
    InputDateBehavior: {}
  }
});

Within a Behavior you have full access to its parent view through this.view property. In fact Behaviors are great proxy objects. All events from the view get proxied through and reversely you can trigger events on the view. Within your implementation you still have full access to any of the view’s elements by using this.el and this.$.

Behaviors have to be registered globally with the Marionette.Behaviors.behaviorsLookup function so that they can be referenced by name in a view. This is meant as a convenient way of avoiding to manually handle imports. If you want to rely on your own module loader solution (RequireJS etc.) you can do that. Just specify a behaviorClass property in the behaviors hash:

define(['marionette', 'lib/tooltip'], function(Marionette, Tooltip) {
  var View = Marionette.ItemView.extend({
     behaviors : {
        Tooltip : {
          behaviorClass: Tooltip,
          message: "hello world"
        }
     }
  });
});

Conclusion

One of the strengths of regular Backbone is its good documentation. Marionette does not lack behind in this regard. With its latest update to version 2.0 the docs were nicely updated and everyone can contribute to them with a simple pull request.

A few things still require a bit more effort on your part. CollectionViews can only automatically handle one collection. If you need to render more than one override the view’s serializeData function or merge your collections. Occasionally, it would be nice if ItemViews had a reference to their parent object or would proxy their events to a parent, when rendering collections.

We recommend that you have a basic understanding of how regular Backbone works before getting into Marionette. After all it follows a lot of the same patterns and approaches.

Marionette still offers much more than what we covered in this blog post: An application object, global event communication channels, TemplateCache, cached jQuery-Objects in your views and additional event hashes for Model and Collection events.

If you are using Backbone, we strongly recommend that you try Marionette for yourself and experience the code reduction and speedup at first hand.

by Tom Herold


Related posts