Grow your Backbone App without going insane

Backbone's philosophy consists in finding the balance between giving structure to your data and remaining a flexible tool. That is what makes Backbone great, but in order to grow your application you will need to lay down the law and start stacking bricks.

Decoupling your application

When looking at a larger program in its entirety, individual functions start to blend into the background. Such a program can be made more readable if we have a larger unit of organization. Modules divide programs into clusters of code that, by some criterion, belong together. Eloquent Javascript

The Module Pattern is a great way to organize your code, keep your namespace clean and allow code reuse making your application easy to reason about.

In a previous article we suggested using a global nested object for your Backbone Models, Views, Router and so on, while that is perfectly reasonable for simple applications as your application grows you will benefit from encapsulating different parts of your code into modules.

Standards and Module loaders

The two most popular standards for building modules are the CommonJS standard for synchronous module loading born from the server (NodeJS) but can be used in the Client with Browserify and the Asynchronous Module Definition, the most used loader for this is RequireJS which is more browser oriented and allows for more complex loading strategies.

While these are focused on each specific module implementation recent module bundlers like webpack take this a step further. Besides loading your javascript modules (supports both standards) it will also do the same for static assets and bundle them together into chunks that you can split and optimize how they are served.

Universal Module Definition

However we don't want to restrict the use of our modules so we will build them following the Universal Module Definition which as the name implies allows them to be loaded by any loader or instead will fallback to attaching the module to the global.

// We encapsulate our module on an intermediate function that will recognize 
// in which environment is working and define the module accordingly.
// The Backbone and Mustache dependencies are explicitly declared and
// required.
(function (root, factory) {
  // AMD
  if (typeof define === 'function' && define.amd) {
    // Register as an anonymous module
    define(['backbone', 'mustache'], factory);
  // CommonJS 
  } else if (typeof module === 'object' && module.exports && require) {
    // Note that this definition does 
    // not support circular dependency from strict CommonJS
    module.exports = factory(require('backbone'), require('mustache'));
  // Globals
  } else {
    // Attach my module to browser globals (root is window)
    root.MyModule = factory(root.Backbone, root.Mustache);
  }
})(this, function (Backbone, Mustache) {
  // Your module code here using Backbone and Mustache.
  // Instead of an object you may also return a function
  var MyModule = {}; 
  return MyModule;
});

For in depth review of the different module definitions and upcoming native implementation on ES6 take a look into Writing Modular JS from Addy Osmani.

Let's get building

To illustrate the benefits of the modular approach we will implement a straight forward internationalization module that is meant to be attached to a Backbone App. If you are not familiar with Backbone I suggest you first take a quick look into the Backbone basics. It will also help if you have used Mustache before since we will use that to generate the views.

We want to be able render the view with different translations depending on the user location or settings. But we don't want our module to be attached to a specific user model, we want a to provide a simple interface to set translations and all the application views should reflect the new translations.

To keep things simple our translations structure will be a key to translation object, we can always introduce complexity later if needed be.

var portuguese = {  
 "Hello World!": "Olá Mundo!"
};
var dutch = {  
 "Hello World!": "Hallo Wereld!"
};

We can use the Backbone.Model to host our translations and extend on it to add our module features. From Backbone we will get a complete API to our translations, we will be able to set attributes (translations), retrieve them from a database, and so on. To retrieve a translation we could just use the .get(key) method but if there are no translations for a specific key this method will return undefined. So lets start by adding an translate method that will instead return the key in this case.

var MyModule = Backbone.Model.extend({  
    /**
     * @param {string} key phrase
     * @returns {string} translation or key phrase if translation not found
     */
    translate: function (key) {
      return this.get(key) ? this.get(key) : key;
    }
  });

Now we need to figure out a way to provide this translations to the views. We can expect the application to provide our module with a template of the view and then it should render according to the translations provided. We be using Mustache to render them.

// initialize and set translation
new MyModule();  
MyModule.set(portuguese);

// create view context with translated content
var view = {  
  title: MyModule.translate('Hello World!'),
};

 // Our translation is now complied to the output variable
 // and can be used to render a view
var output = Mustache.render('<h1>{{title}}</h1>', view);  

With this we got a working version of our module. But this implementation will require you to provide the view object already translated. Lets instead provide the view with our module translation function directly.

var view = {  
  title: 'Hello World!',
  translate: function () {
    return function (text, render) {
      return MyModule.translate(render(text));
    };
  },
};

var output = Mustache.render('<h1>{#translate}{{title}}{/translate}</h1>',  
  view);

If assume that we will be using the same translation in our views why not just inject this function to all our views? This is where your module will start to be less flexible, and you will need to design your application accordingly. But remember for your modules to shine you will need to start making decisions on your application design. We will make use of "monkey-patching" on the Mustache.render method to plugin our module's translate method.

var MyModule = Backbone.Model.extend({  
  // Method called when i18n model is initialized with new constructor
  initialize: function () {
    var self = this;

    // define default mustache render at initialization
    var defaultRender = Mustache.render;

    // Monkey-patch Mustache render to include on all views the
    //  translations (_i18n_)
    Mustache.render = function (template, view) {
      var i18n = function () {
        return function (text, render) {
          // render text and attempt to translate it
          return self.translate(render(text));
        };
      };
      if (!view) {
        return defaultRender.apply(this, [template, {
          _i18n_: i18n
        }]);
      }
      view._i18n_ = i18n;
      return defaultRender.apply(this, arguments);
    };
  },
  /**
   * @param {string} key phrase
   * @returns {string} translation or key phrase if translation not found
   */
  translate: function (key) {
    return this.get(key) ? this.get(key) : key;
  }
};

Now we will only need to modify our templates, initialize the module and provide it with the translations and our views will be translated accordingly when rendered.

var output = Mustache.render('{{#_i18n_}}{{title}}{{/_i18n_}}',  
  {title: 'Hello World!'});

For a working example and complete plugin take a look into our plugin repository Backbone-i18n.

Following the same approach we also built an authorization module Backbone-Auth, that will allow you to render the view according to the current user roles. Here's a quick usage example:

  // current user roles/authorizations, if key is present it will render
  // the corresponding view
  var authorizations = ['title'];
  // Initialization of our Authorization module
  var auth = new Auth();
  var GreetingView = Backbone.View.extend({
    el: document.getElementById('title'),
    render: function () {
      this.$el.html(Mustache.render(
        '{{#_auth_.title}}{{title}}{{/_auth_.title}}' +
        // this will be rendered instead if the user is not authorized
        '{{^_auth_.title}}Not authorized to see title{{/_auth_.title}}', {
          title: 'Hello World!'
        })
      );
      return this;
    }
  });

  auth.set(authorizations);
comments powered by Disqus