1. Introduction

I’d like to start this post with a disclaimer: this is my first Javascript development so hopefully I won’t say anything silly. If I do, I’d love to here about it in the comments! 🙂

I’m currently developing a set of third party Javascript widgets and opted for Backbone.js as the main library supported by Lo-dash (rather than underscore.js). Shortly after beginning I started seeing the need for rendering nested views to maximise re-use and be as DRY as possible. This is a problem I’m sure many people developing a medium to large sized application with Backbone will face.

All the solutions I tried (described below) fell short of actually working. All attempts hit one of the following issues:

  • Re-rendering the container view was removing all the nested views’ events
  • Re-rendering the container view was causing the nested views to be destroyed and then re-initialised.

 

2. What do the tutorials say?

Based on the tutorials I found on Google I created my View’s render method as follows:

render: function() {
    this.$el.html( this.template() );
    return this;
}

This solution works as expected if you don’t have any nested views. However, as soon as you introduce nested views and call the render method more than once, you will notice that your nested views lose their events on the second render. After investigating the .html() method I discovered it calls the .empty() method first which in turn removes all the events associated to the child nodes. This is clearly not great (especially if like me your container view was bound to be rendered multiple times).

 

3. So, how do I keep my events?

Ok, so I went back to the drawing board and started thinking about how I could maintain my events. I came across a relatively simple solution: re-initialise the nested view before rendering it. The code for that looks like this:

render: function() {
    this.$el.html( this.template() );

    this.nestedView = new NestedView();
    this.$el.append( this.nestedView.render().el );

    return this;
}

Perfect! So this keeps all my events intact then, we’re done! Not quite.

This simply creates a new view with a new set of events to attach, so we’re navigating around the actual problem. Furthermore, this will destroy the original nested view and completely re-initialise it. This has two nasty side effects: 1) the state of the view is lost, 2) the view needs to be completely re-initialised and re-rendered (overkill warning).

This solves the events issue but opens a whole new can of worms. Imagine having a multitude of nested views, each fetching initialisation data from your server or doing all kinds of complicated initialisation stuff. This is an ugly solution.

 

4. How about delegating those events?

Having tried the disastrous approach above I decided to focus in on the events and how I could ensure they are rebinded once the nested view is rendered. I thought about using jQuery which offers several functions to deal with events: .bind(), .delegate(), .on(), etc. I tried different combinations using these methods but it quickly became apparent that undelegating and delegating events manually on all views would be messy and definetely hard to maintain.

So I went back to Backbone to look for answers and I found that the View has three very pertinent functions: .delegateEvents(), .undelegateEvents() and .setElement(). Eureka! After reading the documentation and looking at the methods, it was clear that setElement was going to be a winner. Here is the documentation quoted:

If you’d like to apply a Backbone view to a different DOM element, use setElement, which will also create the cached $el reference and move the view’s delegated events from the old element to the new one.

Ok, so the code? Here it is:

render: function() {
    this.$el.html( this.template() );
    this.nestedView.setElement( this.$el.find('.selector') ).render();
    return this;
}

 

5. Tidying things up

Now, I have a number of nested views and I didn’t feel like writing that code over and over again. Also I have to perform a few other operations when nested views are rendered. Luckily I had already extended the default Backbone View (I needed to fetch my templates remotely, hopefully that will be a blog soon!) and so I simply added a new function to my BaseView which could be called by all the Views extending it. Below is the code to extend the default Backbone View with our new function and an example of a CustomView using it:

var BaseView = Backbone.View.extend({

	// Other code here...

	renderNested: function( view, selector ) {
		var $element = ( selector instanceof $ ) ? selector : this.$( selector );
		view.setElement( $element ).render();
	}
});

var NestedView = Backbone.View.extend({
	render: function() {
		this.$el.html( "<strong>I'm nested!</strong>" );
		return this;
	}
});

var CustomView = BaseView.extend({

	initialize: function() {
		this.nestedView = new NestedView();
	}

	render: function() {
		this.$el.html( this.template() );
		this.renderNested( this.nestedView, '.selector' );
		return this;
	}
});

There it is in all it’s glory. I must admit, especially when playing with the jQuery event functions, this got very messy. So I’m happy with the final solution as it’s clean and simple.

I’d love to know if this can be improved though!

 

 

Alessandro