Skip to content

Instantly share code, notes, and snippets.

@krawaller
Created February 29, 2012 06:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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(); // call eventual user-defined cleanup func
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 four 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
4) perform eventual further cleaning up of view special behaviour
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, detailed in the
next section.
The fourth point is rarely needed, but if you do need to clean up some non-event-related stuff, you can
define a "onClose" function on the view, and this will then be called from "close".
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 mandatory '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: $(this.viewContainers[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.empty().append(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 publishes two views in each route function; 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 it then 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).
*/
myApp.router = Backbone.Router.extend({
routes: {
"": "home",
"about": "about",
"article/:id":"article"
},
viewContainers: {
"main": "#main",
"sidebar": "#sidebar",
"navbar": "#navbar"
},
initialize: function(opts){
this.articles = new myApp.articleCollection;
this.articles.fetch();
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);
},
article: function(id){
var article = this.articles.get(id);
this.publishView(new myApp.articleView(article));
this.publishView("sidebar",new myApp.articleSummaryView(article));
},
about: function(){
this.publishView(new myApp.aboutView);
this.publishView("sidebar",new myApp.contactInfoView);
}
},Modules.cleanUpRouter);
/*
ADDING GRACEFUL HANDLING OF STATIC VIEWS
One thing sticks out like a sore thumb here; we are instantiating new views every time we want to
publish them, even if the rendered result will be exactly the same as when the view was previously
shown. If we look over our router functions, we can see that the only time we really need to make
new views is in the "article" function, since those views are provided with a specific article
instance. All the other views are in effect static, and could be cached.
Let us therefore add the concept of a 'viewInstances' object that can be provided upon router
definition, much like the viewContainers object. Similar to that, our 'viewIstances' object should
contain a name-instance pair for each static view. When we later want to publish one of these views, we can
simply refer to them by the name key.
Here we have updated the cleanupRouter module to allow the publishView function to be called with a static
view name instead of a view instance. We also need to give the static views special treatment:
1) We only want to call their render function the first time
2) We don't want to remove their events when overwriting them, but merely temporarily uncouple it from the DOM.
We accomplish this through adding a flag on the static view the first time we publish it, and call its render
function at the same time. If the flag is already there, this is a previously published static view, and we
don't need to render it.
When we overwrite a view, we also look for the flag. If it is there we do not use the "close" function, but
merely "detach" it.
We could set up the static views in our first metaprogramming bootstrap version of "publishView", where we loop
the 'viewContainers' object. However, by doing the work in the actual "publishView" function, we allow for
static views to be added to the 'viewInstances' object during the lifetime of the router, and not just at the
initial definition.
*/
Modules.cleanUpRouter = {
publishView: function(){
for (var cname in this.viewContainers){
this.viewContainers[cname] = {$el: $(this.viewContainers[cname])};
}
this.publishView = function(containername,view){
if (!view){
view = containername;
containername = "main";
}
if (typeof view === "string"){
view = this.viewInstances[view];
if (!view._isRenderedStaticView){
view._isRenderedStaticView = true;
view.render();
}
} else {
view.render();
}
cur = this.viewContainers[containername];
if (cur.view){
if (!cur.view._isRenderedStaticView){
cur.view.close();
} else {
$(cur.view.el).detach();
}
}
cur.view = view;
cur.$el.empty().append(view.el);
this.trigger("publish:"+containername,view);
};
this.publishView.apply(this,arguments);
}
};
/*
We can now update our router code to cache all views except for the ones in the "article" route function.
Even the navbar can be cached, since we can access the instance through the 'viewInstances' object!
*/
myApp.router = Backbone.Router.extend({
routes: {
"": "home",
"about": "about",
"article/:id":"article"
},
viewContainers: {
"main": "#main",
"sidebar": "#sidebar",
"navbar": "#navbar"
},
viewInstances: {
"nav": new myApp.navView,
"home": new myApp.homeView,
"logo": new myApp.logoView,
"about": new myApp.aboutView,
"contactinfo": new myApp.contactInfoView
},
initialize: function(opts){
this.articles = new myApp.articleCollection;
this.articles.fetch();
this.publishView("navbar","nav");
this.on("publish:main",function(view){
this.viewInstances.nav.setSection(view);
},this);
},
home: function(){
this.publishView("home");
this.publishView("sidebar","logo");
},
article: function(id){
var article = this.articles.get(id);
this.publishView(new myApp.articleView(article));
this.publishView("sidebar",new myApp.articleSummaryView(article));
},
about: function(){
this.publishView("about");
this.publishView("sidebar","contactinfo");
}
},Modules.cleanUpRouter);
/*
DISCUSSION
All three issues mentioned initially has now been adressed; we no longer have to worry about creating memory leaks or
leaving behind unwanted event listeners, and at the same time the view publication process was streamlined!
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment