Monday, September 23, 2013

Controlling the Container Element for Backbone Views

I always like when there is more than one way to do the same thing. Flexibility and choice is always good. It can, at times, be confusing which approach to use - especially for beginners. When starting out with Backbone, I spent a considerable amount of time trying to determine the "best-practice" to rendering a view and adding it to the DOM. It was clear in the documentation that more than one method could be used, but after reading through some examples, the following scenario seemed the most common:

First, define the view by rendering the template into the cached container element which will be created automatically when the view is initialized:

var myView = Backbone.View.extend({

   initialize: function( options ) {

      this.template = _.template( $( '#script-block-with-template' ).html() );
   },

   render: function() {

      this.$el.html( this.template( this.model.toJSON() ) );
      
      return this;
   }
});


However, it is not attached to the DOM yet. As part of instantiating and rendering it, you need to insert it to the correct place in the DOM:

   view = new myView({ model: data });
   $( '#parent-container' ).append( view.render().$el );


This is why the documentation suggests returning this in the render function. It allows easy chaining to $el so it can be added to the DOM. Using the default behavior, Backbone will create an empty DIV element into which you can render your content. If you want to override that behavior, the suggested method is to set various properties on the view definition which will control the generation processing:

var myView = Backbone.View.extend({
   
   tagName: 'ul',
   className: 'list-group'

   ...
});



I used this approach for a while but eventually found several problems that have made me reconsider how to define the containing element:
  1. The rendering is performed on a detached element. There are various limitations you will face when trying to perform additional processing on the rendered content before its inserted into the DOM. One of the issues I encountered was in some jQuery UI widgets that scan the DOM looking for certain conditions. If your element is not there, functionality you expected to be enabled is not actually available. This requires you to add special handling to ensure the logic is properly initialized.
  2. The containing element is not in the natural flow of the HTML templates. I generally mock up my structure and then slice it up into templates for each view. I want to be able to read through the template HTML and see the structure. The problem with the view creating the element is you get an extra level added. You can ensure the element fits into the structure by using tagName, className, etc but that takes the structure out of the templates and puts some of it into the view.


Given those two issues, I decided to set the container element myself when creating an instance of the view:

   view = new myView({ el: $( '#view-container' ), model: data });
   view.render();


By using the el option, I can pick the point in the DOM where I want the view attached. The element already exists so I won't have those issues related to detached elements and the content is defined in the flow of my templates. The only gripe you might have about that approach is that the container is not in the template for the view. It would have to be defined in an outer template so it can be selected prior to creating the new view and its content. You could keep it all together in the template and use setElement() to replace the default container:

var myView = Backbone.View.extend({

   initialize: function( options ) {

      this.template = _.template( $( '#script-block-with-template' ).html() );
   },

   render: function() {
   
      // this.$el will now be the top-most element in the template
      // with all the events bound to it.
      // this.$ will also start at that element as well.
      this.setElement( $( this.template( this.model.toJSON() ) ) );
      
      return this;
   }
});



That solution, however, brings us back to the problem of a detached element and this pattern for inserting it into the DOM:

   view = new myView({ model: data });
   $( '#parent-container' ).append( view.render().$el );


It also does extra work since the default element is created during initialization and then just replaced by the new content using setElement(). I've limited the use of this approach to situations where I'm using a library that wants to insert my view's element into the DOM. Otherwise, my preference is to pass in the container to the view upon creation.

As you can see, there are many ways to accomplish the same result in Backbone. What you choose might be a matter of preference. For me, it was based on keeping my content separate from the view definitions and ensuring I was performing operations on elements that were already in the DOM.