Skip to content

Instantly share code, notes, and snippets.

@krawaller
Created February 29, 2012 06:48
Show Gist options
  • Save krawaller/1938608 to your computer and use it in GitHub Desktop.
Save krawaller/1938608 to your computer and use it in GitHub Desktop.
Backbone Sanitary View Publishing Pattern
/*
S A N I T A R Y V I E W P U B L I C A T I O N P A T T E R N
This pattern adresses three issues; memory leaks, involuntarily allowing unwanted event listeners to live
on, and the cumbersome process of publishing a view to the page. The first two issues are dealt with through
making sure that previously published views are removed properly, and not merely have their html
overwritten. And, as will see, the solution to fixing the removal process will also mean a streamlining
of the publication process!
We do this through the use of two mixin modules; one for our views, and one for our router.
THE VIEW MODULE
The view module takes care of cleaning up properly after a view when it is no longer needed.
We accomplish this through adding two functions: "close" and "listenTo".
*/
Modules.cleanUpView = {
_boundEvents: [],
listenTo: function(obj,event,callback,context){
obj.on(event,callback);
this._boundEvents.push({obj:obj,evt:event,fun:callback,ctx:context});
},
close: function(){
this.remove();
this.off();
this.onClose && this.onClose();
var e, evts = this._boundEvents;
while(e=evts.shift()){
e.obj.off(e.evt,e.fun,e.ctx);
}
}
};
/*
THE CLOSE FUNCTION
This function is intended to be called upon view removal. It takes care of three things;
1) remove the DOM element cleanly
2) remove listeners added to the view by other objects
3) remove listeners added by the view on other objects
The first point is easily accomplished through use of the view instance method "remove", which uses jQuery
to ensure full removal across all browsers.
The second point, removing listeners from foreign objects, is also easy - we simply call "off" on the view
instance with no arguments, which will remove all listeners added to it.
The third point is trickier, and also the reason behind the second function in the module:
THE LISTENTO FUNCTION
The "listenTo" function is merely a proxy to "on", and used in the exact same way. However, "listenTo" also
adds the callback to an internal memory array. This array can then be traversed in the "close" function,
enabling us to remove all the listeners used by our view.
USING THE MODULE
When using this module, the responsibility of the coder is two-fold;
1) use "listenTo" instead of "on" when binding events
2) make sure that "close" is called when the view is no longer needed.
Here is a cropped example of the module in action, showing the use of "listenTo". The close function,
of course, would not be used inside the view itself, but called remotely from the Router that does
the publishing.
*/
MyView = Backbone.View.extend({
initialize: function(o){
listenTo(this.model,"change:name",this.updateField,this);
},
updateField: function(model){
this.$(".name").html(model.name);
}
},Modules.cleanUpView);
/*
THE ROUTER MODULE
The second coder responsibility mentioned in the cleanUpView module - making sure that "close"
is called on a view when it is going to be removed/overwritten - is something this Router
module will automate. It does this through adding a "publishView" function, intended to be used
when publishing a view to the page. This function also has the added benefit of hiding away
the call to render and adding of the element.
We also add the concept of a "viewContainers" object, which should be provided when defining the router class.
This object contains a name-selector pair for each container we want to publish views to. This lets us cache the
container elements. It also lets us refer to the container by name when we want to publish to them, which is
sometimes clearer than using the more abstract selector.
*/
Modules.cleanUpRouter = {
publishView: function(){
for (var cname in this.viewContainers){
this.viewContainers[cname] = {el: $(o[cname])};
}
this.publishView = function(containername,view){
if (!view){
view = containername;
containername = "main";
}
cur = this.viewContainers[containername];
if (cur.view){
cur.view.close();
}
cur.view = view;
view.render();
cur.el.html(view.el);
this.trigger("publish:"+containername,view);
};
this.publishView.apply(this,arguments);
}
};
/*
Through some metaprogramming, we make sure that the first call to "publishView" will process the "viewContainers"
object and cache the DOM references. Subsequent calls will use those references to add the html of the view to
be published.
The publishView function is called with a container name and a view instance. If a view has
previously been published to that container, the former view will be cleaned up through a
call to its "close" function.
Since it is quite common to frequently publish views to a single 'main' container, we allow "publishView" to be
called with just a view, in which case the 'main' container is used by default.
We also publish a 'publish:<containername>' event when a view is shown, providing the view instance as event data.
USING THE MODULE
First off, when defining the router, we provide a 'viewContainers' object containing name-selector
pairs for all containers we are going to publish views to.
The various route callbacks then become very clean, since all we need to do is call the
publishView function. This takes care of rendering the view, attaching the element to the DOM,
and cleaning up an eventual previous view published to the same container.
This particular app always publishes two views; one to the main area, and another to a sidebar,
probably with some info related to what will be shown in the main area.
This router also uses the publish event from the publishView function to set up a navigation bar
functionality. This navigation bar needs to be changed whenever something is published to the 'main'
portion of the page, since the navigation bar wants to highlight our position.
This functionality is hooked up in the initialize function, where we instantiate and publish the navbar.
Then we set a listener to the Router's own 'publish:main' event. Upon catching that event we call the
"setSection" function on our navbar view. We provide the view instance that was published, which
"setSection" can use to determine what section to highlight (presumably through a 'name' or 'section'
property on that particular view).
*/
MyRouter = Backbone.Router.extend({
routes: {
"": "home",
"about": "about"
},
viewContainers: {
"main": "#main",
"sidebar": "#sidebar",
"navbar": "#navbar"
},
initialize: function(opts){
this.navView = new myApp.navView;
this.publishView("navbar",this.navView);
this.on("publish:main",function(view){
this.navView.setSection(view);
},this);
},
home: function(){
this.publishView(new myApp.homeView);
this.publishView("sidebar",new myApp.logoView);
},
about: function(){
this.publishView(new myApp.aboutView);
this.publishView("sidebar",new myApp.contactInfoView);
}
},Modules.cleanUpRouter);
/*
DISCUSSION
This pattern has won us three victories; we no longer have to worry about creating memory leaks &
leaving behind unwanted event listeners, and at the same time the view publication process was
streamlined!
Further development of these modules might include dealing with static views; views that contain
no dynamic info, and therefore doesn't need to be fully cleaned up and rebuilt.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment