Skip to content

Overview

chug2k edited this page Nov 13, 2013 · 9 revisions

This tool is designed to make your time with Backbone.View easier and more powerful. It is an opinionated approach to structuring your web application.

Philosophy

The design goals of LayoutManager are oriented around being an extension of Backbone. This means that we follow the doctrine promoted by the project. Much like Backbone this project is low level and doesn't solve all problems you may encounter when developing an application. This allows it to be more flexible and customizable.

When you develop a new application by yourself or with a team, choices are made as to which tools to utilize. It's entirely possible that you switch tools during development, because needs are better met with something else. When you use LayoutManager you will have full flexibility to use any DOM library and template engine.

There are many other solutions that attempt to solve the View management problem. You may encounter projects like Marionette, Chaplin, Thorax, ViewMaster, etc. It's very worthwhile to research and investigate what problem each project is attempting to solve. For instance if you want deep View integration with Handlebars out-of-the-box, you should look into Thorax. If you want a very opinionated way of structuring decoupled applications, Marionette might be what you're looking for. If you are more comfortable working with CoffeeScript, Chaplin may be up your alley.

LayoutManager is for you if you are looking for a plugin to augment Backbone and provide essential lacking features like: full template lifecycle, structuring layouts, ensuring proper cleanup for all Views, and giving you control of your nested Views with a concise API.

How to write LayoutManager

This is an artificial narrative, I don't know who you are or what your skill level is, so this is written assuming you're completely green to Backbone development.

When getting started with Backbone for the first time, it is not immediately obvious how to manage your Views. Posts explaining how to render a template often look like this:

var MyFirstView = Backbone.View.extend({
  render: function(data) {
    // Render the template with data passed to `render`.
    var markup = _.template("<%= name %>")(data);
    
    // Put the content into this Views element.
    this.$el.html(markup);

    // Allow chaining.
    return this;
  }
});

From here you can initialize and render the View:

// Create a new instance.
var myFirstView = new MyFirstView();

// Insert into the Document.
myFirstView.$el.appendTo("body");

// Render the View with the name `Tom Branton`.
myFirstView.render({ name: "Tom Branton" });

Since this is a common task you will typically abstract this into a BaseView with some other defaults as well to make creating new Views easier.

var BaseView = Backbone.View.extend({
  // Override this property to specify the compiled template function.
  template: _.template(""),

  // Override this method to provide the right data to your template.
  serialize: function() {
    return {};
  },

  // This method now looks for the above `template` and `serialize` properties
  // in order to render.
  render: function() {
    // Render the template markup.
    var markup = this.template(this.serialize());

    // Put the content into this Views element.
    this.$el.html(markup);

    // Allow chaining.
    return this;
  }
});

Now your MyFirstView definition would look like this instead:

var MyFirstView = BaseView.extend({
  template: _.template("<% name %>"),

  serialize: function() {
    return { name: "Tom Branton" };
  }
});

This works great and for many this is where abstraction ends. This appears fine when your template is a single property. Imagine now the template is a product description page. It's not maintainable to keep the template inline like this.

var MyFirstView = BaseView.extend({
  template: _.template("<h4><%= name %></h4><em>Price: <%= price %><hr><%= description %><hr><h5>Details:</h5><table><% details.each(function(detail) { %><tr><td><%= detail.name %></td><td><%= detail.value %></td></tr><% }); %></table>")
});

Googling leads around you to a few solutions and the most common approach appears to be using HTML <script> tags with a custom type attribute to stop the browser from attempting to execute the contents. The template now looks like:

<script id="productDescription" type="text/template">
<h4><%= name %></h4>
<em>Price: <%= price %>

<hr>
<%= description %>
<hr>

<h5>Details:</h5>
<table>
  <% details.each(function(detail) { %>
    <tr>
      <td><%= detail.name %></td>
      <td><%= detail.value %></td>
    </tr>
  <% }); %>
</table>
</script>

This is slightly annoying, because as you can see above the syntax highlighting is busted since your editor thinks it's JavaScript. This isn't a huge problem, and you decide it's sufficient, so you modify your BaseView to utilize the DOM element now.

var BaseView = Backbone.View.extend({
  // Override this property to specify the template selector.
  template: "",

  // Override this method to provide the right data to your template.
  serialize: function() {
    return {};
  },

  // This method now looks for the above `template` and `serialize` properties
  // in order to render.
  render: function() {
    // Get the contents from the script tag and compile the function.
    var template = _.template($(this.template).html());
    // Render the markup.
    var markup = template(this.serialize());

    // Put the content into this Views element.
    this.$el.html(markup);

    // Allow chaining.
    return this;
  }
});

This works great so you start fleshing out the rest of your application and as you continue developing, the templates start building up in your HTML. It starts to become a pain, because you cannot structure and group the templates that you want to work on. You go back to Googling again and find that others are fetching templates remotely to preserve syntax highlighting, increase organization, maintain cleaner HTML, assist with development and debugging, etc.

This sounds awesome, but now you're stuck deciding what the best way to implement this is. Up till now everything has been synchronous, but now you're introducing an asynchronous operation into your View rendering.

There are a bunch of ways to implement this, but you decide to go with something relatively easy to understand:

var BaseView = Backbone.View.extend({
  // Override this property to specify the path to the template.
  template: "",

  // Override this method to provide the right data to your template.
  serialize: function() {
    return {};
  },

  // Overrideable fetch method to get the template contents, pass a callback to
  // get the contents.
  fetch: function(done) {
    // Using jQuery, execute a GET request to asynchronously load the template
    // from the filesystem.
    $.get(this.template, function(contents) {
      // Trigger the callback with the compiled template function.
      done(_.template(contents));
    });
  },

  // This method now looks for the above `template` and `serialize` properties
  // in order to render.
  render: function() {
    // Fetch the template.
    this.fetch(function(template) {
      // Render the markup.
      var markup = template(this.serialize());

      // Put the content into this Views element.
      this.$el.html(markup);
    });

    // Allow chaining.
    return this;
  }
});

Your View gets refactored to now look like this:

var MyFirstView = BaseView.extend({
  // The path to where the template lives on your server.
  template: "/templates/product/description.html",

  // You have a model with product description data to use in the template.
  serialize: function() {
    return this.model;
  }
});

Your code creating the View instance now looks like this:

// Create a new instance.
var myFirstView = new MyFirstView();

// Insert into the Document.
myFirstView.$el.appendTo("body");

// Render the View with the name `Tom Branton`.
myFirstView.render();

This is cool and works, except...

  • Now you're fetching templates every time you create a new View which is expensive even if they get cached by HTTP.

    • So you implement template caching in BaseView.
  • Now your jQuery plugins don't work, because they don't know when your View has finished rendering.

    • So you implement beforeRender and afterRender events/callbacks.
  • You want to ensure proper cleanup of your Views, because you're constantly creating and destroying them.

    • So you implement a cleanup method to handle common cases and allows you specify your own function.
  • You decide to break up your large templates into smaller more manageable ones, but now you need a way to manage the relationships.

    • So you implement setView and insertView methods to replace or append a nested View.
  • You decide to use a different rendering engine.

    • So you implement the ability to swap out the engine used with a render method.
  • You hate how Backbone wraps every View with a <div> and would prefer to use the top level element in your template.

    • So you create an option to automatically call setElement on the first child element inside the template.
  • You want to share your Views with a Node.js server.

    • So you research the best practices and implement the necessary configuration as a reusable node module.

Congratulations, you've just written LayoutManager!