-
-
Save chchrist/2412935 to your computer and use it in GitHub Desktop.
Backbone Sanitary View Publishing Pattern
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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