Skip to content

Instantly share code, notes, and snippets.

@krawaller
Created February 29, 2012 06:48

Revisions

  1. krawaller revised this gist Apr 14, 2012. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -99,7 +99,7 @@ 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.viewContainers[cname] = {$el: $(this.viewContainers[cname])};
    }
    this.publishView = function(containername,view){
    if (!view){
    @@ -230,7 +230,7 @@ initial definition.
    Modules.cleanUpRouter = {
    publishView: function(){
    for (var cname in this.viewContainers){
    this.viewContainers[cname] = {$el: $(o[cname])};
    this.viewContainers[cname] = {$el: $(this.viewContainers[cname])};
    }
    this.publishView = function(containername,view){
    if (!view){
  2. krawaller revised this gist Feb 29, 2012. 1 changed file with 7 additions and 2 deletions.
    9 changes: 7 additions & 2 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -35,18 +35,23 @@ Modules.cleanUpView = {
    /*
    THE CLOSE FUNCTION
    This function is intended to be called upon view removal. It takes care of three things;
    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:
    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
  3. krawaller revised this gist Feb 29, 2012. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -94,7 +94,7 @@ 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.viewContainers[cname] = {$el: $(o[cname])};
    }
    this.publishView = function(containername,view){
    if (!view){
    @@ -107,7 +107,7 @@ Modules.cleanUpRouter = {
    }
    cur.view = view;
    view.render();
    cur.el.empty().append(view.el);
    cur.$el.empty().append(view.el);
    this.trigger("publish:"+containername,view);
    };
    this.publishView.apply(this,arguments);
    @@ -225,7 +225,7 @@ initial definition.
    Modules.cleanUpRouter = {
    publishView: function(){
    for (var cname in this.viewContainers){
    this.viewContainers[cname] = {el: $(o[cname])};
    this.viewContainers[cname] = {$el: $(o[cname])};
    }
    this.publishView = function(containername,view){
    if (!view){
    @@ -250,7 +250,7 @@ Modules.cleanUpRouter = {
    }
    }
    cur.view = view;
    cur.el.empty().append(view.el);
    cur.$el.empty().append(view.el);
    this.trigger("publish:"+containername,view);
    };
    this.publishView.apply(this,arguments);
  4. krawaller revised this gist Feb 29, 2012. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -107,7 +107,7 @@ Modules.cleanUpRouter = {
    }
    cur.view = view;
    view.render();
    cur.el.html(view.el);
    cur.el.empty().append(view.el);
    this.trigger("publish:"+containername,view);
    };
    this.publishView.apply(this,arguments);
    @@ -250,7 +250,7 @@ Modules.cleanUpRouter = {
    }
    }
    cur.view = view;
    cur.el.html(view.el);
    cur.el.empty().append(view.el);
    this.trigger("publish:"+containername,view);
    };
    this.publishView.apply(this,arguments);
  5. krawaller revised this gist Feb 29, 2012. 1 changed file with 5 additions and 4 deletions.
    9 changes: 5 additions & 4 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -205,14 +205,15 @@ 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 them the first time
    2) We don't want to remove their events when overwriting them, but merely uncouple it from the DOM.
    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, and if it is there we do not use the "close" function.
    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
    @@ -245,7 +246,7 @@ Modules.cleanUpRouter = {
    if (!cur.view._isRenderedStaticView){
    cur.view.close();
    } else {
    cur.view.remove();
    $(cur.view.el).detach();
    }
    }
    cur.view = view;
  6. krawaller revised this gist Feb 29, 2012. 1 changed file with 30 additions and 5 deletions.
    35 changes: 30 additions & 5 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    /*
    S A N I T A R Y V I E W P U B L I S H I N G P A T T E R N
    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
    @@ -203,8 +203,22 @@ definition, much like the viewContainers object. Similar to that, our 'viewIstan
    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:
    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 them the first time
    2) We don't want to remove their events when overwriting them, but merely 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, and if it is there we do not use the "close" function.
    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 = {
    @@ -219,13 +233,22 @@ Modules.cleanUpRouter = {
    }
    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){
    cur.view.close();
    if (!cur.view._isRenderedStaticView){
    cur.view.close();
    } else {
    cur.view.remove();
    }
    }
    cur.view = view;
    view.render();
    cur.el.html(view.el);
    this.trigger("publish:"+containername,view);
    };
    @@ -280,6 +303,8 @@ myApp.router = Backbone.Router.extend({
    },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!
    */
  7. krawaller revised this gist Feb 29, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    /*
    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
    S A N I T A R Y V I E W P U B L I S H I N G 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
  8. krawaller revised this gist Feb 29, 2012. 1 changed file with 108 additions and 14 deletions.
    122 changes: 108 additions & 14 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -24,7 +24,7 @@ Modules.cleanUpView = {
    close: function(){
    this.remove();
    this.off();
    this.onClose && this.onClose();
    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);
    @@ -84,10 +84,11 @@ module will automate. It does this through adding a "publishView" function, inte
    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.
    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 = {
    @@ -137,12 +138,12 @@ The various route callbacks then become very clean, since all we need to do is c
    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 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 the navigation bar wants to highlight our position.
    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
    @@ -152,17 +153,21 @@ property on that particular view).
    */

    MyRouter = Backbone.Router.extend({
    myApp.router = Backbone.Router.extend({
    routes: {
    "": "home",
    "about": "about"
    "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){
    @@ -173,19 +178,108 @@ MyRouter = Backbone.Router.extend({
    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);

    /*
    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!
    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:
    */

    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.
    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";
    }
    if (typeof view === "string"){
    view = this.viewInstances[view];
    }
    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);
    }
    };

    /*
    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);

    /*
    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!
    */
  9. krawaller created this gist Feb 29, 2012.
    191 changes: 191 additions & 0 deletions bbviewpubpattern.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,191 @@
    /*
    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.
    */