Skip to content
Greg MacWilliam edited this page Feb 27, 2014 · 22 revisions

ContainerView provides a robust scaffold for common functional view programming tasks. The design of the library is guided by a few core principles, including...

1. Memory Management

Backbone Views frequently need to bind event listeners. These event bindings must be undone when the view is deprecated, otherwise the view may be held in memory rather than being garbage collected. For this purpose, Backbone Views provide a remove method that cleans up the view's display and its event bindings. It's our job to make sure that remove is called on every view instance when we're finished with it. To accomplish this, we could write a quick managed view system like this:

var MySloppyManagedView = Backbone.View.extend({

  // Create an array for managing a list of subviews:
  initialize: function() {
    this._subviews = [];
  },

  // Remove all existing subviews, and then render new subviews:
  render: function() {
    _.invoke(this._subviews, 'remove');
    this._subviews.length = 0;    

    this.collection.each(function(model) {
      var item = new ItemView({model: model});
      item.render();
      this.$el.append(item.$el);
      this._subviews.push(item);
    }, this);
  },

  // Remove all subviews along with this parent view:
  remove: function() {
    _.invoke(this._subviews, 'remove');
    Backbone.View.prototype.remove.call(this);
  }
});

In the above example, all generated list items are stored in an array of managed subviews. Whenever the parent view is re-rendered or removed, all of its managed subviews have their remove methods invoked. ContainerView provides this managed subview array and performs cleanup tasks on it.

However, the above example has some problems: every subview is being added/removed directly through the DOM, which creates performance bottlenecks. That brings us to...

2. Minimizing DOM Reflows

When elements are added or removed from the active DOM, a document reflow is triggered. Reflow is an expensive operation that forces the browser to recalculate layout. To optimize rendering performance, we want to consolidate DOM updates and thereby trigger as few reflows as possible. This is best done by creating and cleaning up views outside of the active DOM. That could be written something like this:

var MyBetterManagedView = Backbone.View.extend({

  // Create an array for managing a list of subviews:
  initialize: function() {
    this._subviews = [];
  },

  // Render new views outside of the DOM, 
  // swap in the new views, and then clean up the old views:
  render: function() {
    var subviews = [];
    var content = document.createDocumentFragment();

    this.collection.each(function(model) {
      var item = new ItemView({model: model});
      item.render();
      content.appendChild(item.el);
      subviews.push(item);
    }, this);

    this.$el.html(content);
    _.invoke(this._subviews, 'remove');
    this._subviews = subviews;
  },

  // Remove the container element, and then clean up its managed subviews:
  remove: function() {
    Backbone.View.prototype.remove.call(this);
    _.invoke(this._subviews, 'remove');
  }
});

This implementation is far more performant than the first example: new views are being rendered outside of the active DOM, and then swapped into the DOM with a single update. Likewise, the old swapped-out views can be efficiently cleaned up outside of the DOM. This example's remove method is also made more efficient by removing the parent view from the DOM before calling remove on its managed subviews.

ContainerView deliberately organizes its internal workflow around performing bulk rendering and cleanup tasks outside of the DOM. However, the onus remains on the developer to take advantage of these optimizations. Which brings us to...

3. Workflow

Convenient workflow methods make a tool easy-to-use, and enforce the proper use of optimization patterns. ContainerView provides a few common view management and rendering patterns, including:

open/close regions

A ContainerView includes open and close methods for "opening" new managed content into the container. When new content is opened, any existing container content is removed and cleaned up. Single views may be opened into a container, or entire lists:

var container = ContainerView.create('#app-container');

// Open the "home" screen into the container region:
container.open(new HomeView());

// Now open the "about" screen into the container:
// (this replaces the "home" screen, which is then cleaned up)
container.open(new AboutView());

// Now open a rendered list of member items into the container:
// (this new list is rendered, and then replaces the "about" screen)
container.open(MemberItemView, this.membersCollection);

// Now close out all container content:
container.close();

sub-containers

A ContainerView may easily create generic sub-containers. This gives the parent a generic container region to work with in addition to its other interface components. The sub-container will be managed by the parent, so its display tree will be removed along with the parent:

var MyContainer = Backbone.ContainerView.extend({
  initialize: function() {
    this.list = this.createSubcontainer('ul.list-view');
  },

  render: function() {
    if (this.collection.length) {
      this.list.open(ListItemView, this.collection);
    } else {
      this.list.open(new ListEmptyView());
    }
  },

  events: {
    'click .close-list': 'onCloseList'
  },

  onCloseList: function() {
    this.list.close();
  }
});

one-off views

It's pretty common to instance one-off view components within a wrapper view. However, those one-off component instances should be managed by their wrapper so that they're cleaned up along with it.

To add one-off components into a container, you can append managed subviews into the container element's layout:

var MyWrapperView = Backbone.ContainerView.extend({
  initialize: function() {
    // Append feed widget into a specific wrapper element:
    this.append(new FeedWidget(), '.feed-wrapper');

    // Append friends widget into the container's root element:
    this.append(new FriendsWidget());
  }
});

Or, you can swapIn managed subviews for placeholder elements within the container:

var MyWrapperView = Backbone.ContainerView.extend({
  initialize: function() {
    this.swapIn(new FeedWidget(), '#feed-placeholder');
    this.swapIn(new FriendsWidget(), '#friends-placeholder');
  }
});

Next: Installation »