failing approach to dispose off the zombie views in backbonejs - backbone.js

so i had the same famous problem of zombie views in my backbone app. I tried this to become a superhero :P
var Router=Backbone.Router.extend({
routes:{
"":"loadDashboard",
"home":"loadDashboard",
'post-leads':"loadPostLeads"
},
initialize:function(){
window.currentView=null;
},
loadPostLeads:function(){
require(['views/post-leads'],function(leads){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new leads();
window.currentView.render();
})
},
loadDashboard: function(){
require(['views/dashboard'],function(dashboard){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new dashboard();
window.currentView.render();
})
}
});
This doesn't work. I wanted something simple and don't want to use marionette or anything similar for that sake. Whats going wrong above? Is it a sensible approach?

In principle what you do should work, but there are some things that Backbone can't clean up, because it doesn't know of them.
First, you should make sure that you are using a recent version of Backbone (0.9.9 or newer). There have been some improvements to the event binding code, which makes it easier for the View.remove method to do all the necessary cleanup.
The common gotchas are:
Listening to model events:
//don't use other.on (Backbone doesn't know how to clean up)
this.model.on('event', this.method);
//use this.listenTo (Backbone cleans up events when View.remove is called)
//requires Backbone 0.9.9
this.listenTo(this.model, 'event', this.method);
Listening to DOM events outside your view's scope:
//if you listen to events for nodes that are outside View.el
$(document).on('event', this.method);
//you have to clean them up. A good way is to override the View.remove method
remove: function() {
$(document).off('event', this.method);
Backbone.View.prototype.remove.call(this);
}
Direct references:
//you may hold a direct reference to the view:
this.childView = otherView;
//or one of its methods
this.callback = otherView.render;
//or as a captured function scope variable:
this.on('event', function() {
otherView.render();
});
Closures:
//if you create a closure over your view, or any method of your view,
//someone else may still hold a reference to your view:
method: function(arg) {
var self = this;
return function() {
self.something(x);
}
}
Avoiding the following pitfalls should help your views to get cleaned up correctly.
Edit based on comment:
Ah, you didn't mention the full problem in your question. The problem with your approach is, as I gather, is that you're trying to render the two views into the same element:
var View1 = Backbone.View.extend({el:"#container" });
var View2 = Backbone.View.extend({el:"#container" });
And when you remove View1, the View2 does not correctly render.
Instead of specifying the view el, you should render the views into an element. On your page you should have a #container element, and append the view's element into the container.
loadPostLeads: function () {
var self = this;
require(['views/post-leads'], function (leads) {
self.renderView(new leads());
})
},
loadDashboard: function () {
var self = this;
require(['views/dashboard'], function (dashboard) {
self.renderView(new dashboard());
})
},
renderView: function(view) {
if(window.currentView) {
window.currentView.remove();
}
//the view itself does not specify el, so you need to append the view into the DOM
view.render();
$("#container").html(view.el);
window.currentView = view;
}

Related

Backbonejs view binding conceptual feedback

I ran into this article (http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) and was wondering if the idea of binding and rendering views in the router after instantiating them is best practice. I have been binding my views and rendering them in my view definition.
Currently this is how I've been setting up and calling my views:
EmployeeView:
EmployeeView = Backbone.View.extend({
el: '#content',
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
}
My Router:
employeeList: function () {
var c = new EmployeeCollection
new EmployeeView( {
collection: c
});
}
It works fine. But according to the article a better practice is to do the following:
EmployeeView = Backbone.View.extend({
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
return this;
}
Router
employeeList: function () {
var c = new EmployeeCollection
$('#content').html(new EmployeeView( {collection: c}).render().el);
},
I like the solution in the article because it decouples the views from other DOM events as the article said and allows me to focus all my tweaking and customizing in one place, the router. But because I'm passing in a collection/model and need to fetch the data in my initialize my page renders twice. My questions are:
Is this really best practice?
How do I avoid calling the render twice if I want to use the suggested method?
What if I have cases where I have some front end user interaction and then need to refresh the view collection/model? Would I have to do it in my view or could that happen in the router as well?
The view you have here, and the one in the article are totally different.
In your example, the view is bound to an element in DOM (#content),
which is not a good practice, especially for beginners and causes lots of bugs that we see here every day.
For example if you create 2 instances of your view then event will starts firing multiples times and along with that all hell will break loose.
The view in the article creates a new <div> element in memory per instance, which is a good practice.
Now, to add this in DOM, newbies often do stuff like the following inside the view's render:
$('#content').html(this.$el);
This creates a global selector inside the view and makes it aware of the outer world which is not a good practice.
The article probably (I didn't read it) address this is issue and presents and alternative of adding the view element to DOM from the router, which is a good practice in my opinion.
To avoid rendering twice in the code from article you can just do:
$('#content').html(new EmployeeView( {collection: c}).el);
el being a live reference, it'll be updated when the fetch succeeds. .render().el is another common mis-understanding spread by all the existing blogs and tutorials.
Side note: Since we are discussing best practices, omitting the semicolon and parenthesis as in var c = new EmployeeCollection is not a good practice either. Go with var c = new EmployeeCollection();
You got it almost right. You're just rendering it twice, which I don't think is the right way to go, as there is no point.
EmployeeView = Backbone.View.extend({
template:template,
initialize: function(){
console.log("Will print second");
this.collection.fetch({ reset: true });
this.collection.on('reset', this.appendEmployees, this);
},
render: function(){
//this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('Will print 3rd. render called');
return this;
}
appendEmployees: function(){
console.log("Will print 4th. Appending employees");
$(this.el).html(Mustache.to_html(this.template, {employee_list: this.collection.toJSON() });
}
})
Router
employeeList: function () {
var c = new EmployeeCollection()
var view = new EmployeeView({ collection: c });
console.log("Will print 1st");
$('#content').html(view.render().el);
}
First, when you do view.render().el it will append view's element (which will be empty by that time) to #content
Second, you're executing appendEmployees function when collection resets. By the time this will happen your element will already be placed in the DOM.
In case you need to refresh, it can be done inside the view, by calling the appendEmployees function, or even by resetting your collection. Or if you navigate to the same route via backbone, the whole process will be repeated hence your collection will be called again, and the page will render from beginning. So it comes down to your preferences on when/why you'd choose one over the other. Hope this helps.

Two Views Sharing an Event

I've been thrown into a Backbone code base and one of the modifications I need to make requires duplicating a text element with typeahead. Rather than copy and paste code, I'd like to re-use the event code but as I know hardly anything about Backbone I'm not sure how this should be done. Should it be a helper? If so, where do I put the helper code so it can be used by both views? I'd rather not attempt view inheritance if at all possible because I'd like to keep the changes as simple and minimal as possible.
events: {
// all other events removed for conciseness.
'typeahead:selected #ud_producerid': 'producerChanged'
}
I need the same event with the identical functionality in the producerChanged function as well as the setupBindings code that wires up the typeahead to work in 2 different views.
I know you said you didn't want to use inheritance here but it is easy in Backbone and well suited to the task.
var TypeaheadBase = Backbone.View.extend({
events: {
'typeahead:selected #ud_producerid': 'producerChanged'
},
producerChanged: function(e) {
...
},
anotherBaseMethod: function() {
...
}
});
var TypeaheadBaseA = TypeaheadBase.extend({
someOtherAMethod: function() {
...
},
// You can do some extra functionality on `producerChanged`.
// (Or you can override by not calling the Base prototype).
producerChanged: function() {
TypeaheadBase.prototype.producerChanged.apply(this, arguments);
// Do some additional stuff.
}
});
var TypeaheadBaseB = TypeaheadBase.extend({
// You can also extend things like events, which could be a hash (Object).
events: function() {
var parentEvents = _.result(TypeaheadBase.prototype, 'events');
return _.extend({}, parentEvents, {
'click a': 'someClickEvent'
});
},
someClickEvent: function() {
...
}
});

events not firing after re-render in backbone.js

I am facing a problem while trying to click submit after re-render.
This is my view:
ShareHolderInfoView = Backbone.View.extend( {
template : 'shareholderinfo',
initialize: function() {
this.model = new ShareHolderInfoModel();
},
render : function() {
$.get("shareholderinfo.html", function(template) {
var html = $(template);
that.$el.html(html);
});
//context.loadViews.call(this);
return this;
},
events:{
"change input":"inputChanged",
"change select":"selectionChanged",
"click input[type=submit]":"showModel"
},
inputChanged:function(event){
var field = $(event.currentTarget);
var data ={};
data[field.attr('id')] = field.val();
this.model.set(data);
},
showModel:function(){
console.log(this.model.attributes);
alert(JSON.stringify(this.model.toJSON()));
}
});
This is my Router
var shareholderInfo, accountOwnerInfo;
App.Router = Backbone.Router.extend({
routes:{
'share':'share',
'joint':'joint'
},
share:function(){
$("#subSection").empty();
if(!shareholderInfo){
shareholderInfo = new ShareHolderInfoView();
$("#subSection").append(shareholderInfo.render().el);
} else{
$("#subSection").append(shareholderInfo.$el);
}
},
joint:function(random){
$("#subSection").empty();
if(!accountOwnerInfo){
accountOwnerInfo = new AccountOwnerInfoView();
$("#subSection").append(accountOwnerInfo.render().el);
} else{
$("#subSection").append(accountOwnerInfo.$el);
}
}
});
This is my HTML a div with id='subSection'.
if I check in console, I can able to see the events bound to that view.
Object {change input: "inputChanged", change select: "selectionChanged", click input[type=submit]: "showModel"}
But its not calling that showModel function afer i click submit. Please help.
Your fundamental problem is that you're improperly reusing views.
From the fine manual:
.empty()
Description: Remove all child nodes of the set of matched elements from the DOM.
[...]
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So when you say:
$("#subSection").empty();
you're not just clearing out the contents of #subSection, you're also removing all event handlers attached to anything inside #subSection. In particular, you'll remove any event handlers bound to accountOwnerInfo.el or shareholderInfo.el (depending on which one is already inside #subSection).
Reusing views is usually more trouble than it is worth, your views should be lightweight enough that you can destroy and recreate them as needed. The proper way to destroy a view is to call remove on it. You could rewrite your router to look more like this:
App.Router = Backbone.Router.extend({
routes: {
'share':'share',
'joint':'joint'
},
share: function() {
this._setView(ShareHolderInfoView);
},
joint: function(random){
this._setView(AccountOwnerInfoView);
},
_setView: function(view) {
if(this.currentView)
this.currentView.remove();
this.currentView = new view();
$('#subSection').append(this.currentView.render().el);
}
});
If your views need any extra cleanup then you can override remove on them to clean up the extras and then chain to Backbone.View.prototype.remove.call(this) to call the default remove.
If for some reason you need to keep your views around, you could call delegateEvents on them:
delegateEvents delegateEvents([events])
Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
and you'd say things like:
$("#subSection").append(shareholderInfo.$el);
shareholderInfo.delegateEvents();
instead of just:
$("#subSection").append(shareholderInfo.$el);
I'd strongly recommend that you treat your views and cheap ephemeral objects: destroy them to remove them from the page, create new ones when they need to go on the page.

Bug while creating object in View

I'm working on a backbone.js project which is mainly to learn backbone framework itself.
However I'm stuck at this problem which i can't figure out but might have an idea about the problem...
I've got an Create View looking like this...
define(['backbone', 'underscore', 'jade!templates/addAccount', 'models/accountmodel', 'common/serializeObject'],
function(Backbone, underscore, template, AccountModel, SerializeObject){
return Backbone.View.extend({
//Templates
template: template,
//Constructor
initialize: function(){
this.accCollection = this.options.accCollection;
},
//Events
events: {
'submit .add-account-form': 'saveAccount'
},
//Event functions
saveAccount: function(ev){
ev.preventDefault();
//Using common/serializeObject function to get a JSON data object from form
var myObj = $(ev.currentTarget).serializeObject();
console.log("Saving!");
this.accCollection.create(new AccountModel(myObj), {
success: function(){
myObj = null;
this.close();
Backbone.history.navigate('accounts', {trigger:true});
},
error: function(){
//show 500?
}
});
},
//Display functions
render: function(){
$('.currentPage').html("<h3>Accounts <span class='glyphicon glyphicon-chevron-right'> </span> New Account</h3>");
//Render it in jade template
this.$el.html(this.template());
return this;
}
});
});
The problem is that for every single time I visit the create page and go to another and visit it again. It remebers it, it seems. And when i finally create a new account I get that many times I've visited total number of accounts...
So console.log("Saving!"); in saveAccount function is called x times visited page...
Do I have to close/delete current view when leaving it or what is this?
EDIT
Here's a part of the route where i init my view..
"account/new" : function(){
var accCollection = new AccountCollection();
this.nav(new CreateAccountView({el:'.content', accCollection:accCollection}));
console.log("new account");
},
/Regards
You have zombie views. Every time you do this:
new CreateAccountView({el:'.content', accCollection:accCollection})
you're attaching an event listener to .content but nothing seems to be detaching it. The usual approach is to call remove on a view to remove it from the DOM and tell it to clean up after itself. The default remove does things you don't want it to:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
You don't want that this.$el.remove() call since your view is not responsible for creating its own el, you probably want:
remove: function() {
this.$el.empty(); // Remove the content we added.
this.undelegateEvents(); // Unbind your event handler.
this.stopListening();
return this;
}
Then your router can keep track of the currently open view and remove it before throwing up another one with things like this:
if(this.currentView)
this.currentView.remove();
this.currentView = new CreateAccountView({ ... });
this.nav(this.currentView);
While I'm here, your code will break as soon as you upgrade your Backbone. As of version 1.1:
Backbone Views no longer automatically attach options passed to the constructor as this.options, but you can do it yourself if you prefer.
So your initialize:
initialize: function(){
this.accCollection = this.options.accCollection;
},
won't work in 1.1+. However, some options are automatically copied:
constructor / initialize new View([options])
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.
so you could toss out your initialize, refer to this.collection instead of this.accCollection inside the view, and instantiate the view using:
new CreateAccountView({el: '.content', collection: accCollection})

Routing & events - backboneJS

How should I be handling routing in BackboneJS? When routing, after new-upping my view, should I be triggering an event, or rendering the view directly?
Here are the two scenarios:
Trigger Event:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
var viewOrderView = new ViewOrderView();
vent.trigger('order:show', orderId);
}
In my view, I have:
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function () {
vent.on('order:show', this.show, this);
},
show: function (id) {
this.id = id;
this.render();
},
render: function () {
var template = viewOrderTemplate({ id: this.id });
this.$el.html(template);
return this;
}
});
OR, should I go this route:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
var viewOrderView = new ViewOrderView({id : orderId });
viewOrderView.render();
}
In my view, I have:
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function () {
//init code here
},
render: function () {
var template = viewOrderTemplate({ id : this.id});
this.$el.html(template);
return this;
}
});
I think it's the first scenario - given that backbone is event driven, but the 2nd obviously has less code.
Also, I suppose a third scenario would be to keep the view code in the first scenario, but grab the router scenario of the second... rendering the view on navigation, but exposing an event in case I want to trigger that elsewhere.
Thoughts?
So all backbone questions usually end up with many plausible answers. In this case, I believe your second example is a more canonical/typical backbone pattern. Putting aside the tricky issue of handling loading spinners and updating after data loads, the simplified basic pattern in your router would be:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
//Use models to represent your data
var orderModel = new Order({id: orderId});
//models know how to fetch data for themselves given an ID
orderModel.fetch();
//Views should take model instances, not scalar model IDs
var orderView = new OrderView({model: orderModel});
orderView.render();
//Exactly how you display the view in the DOM is up to you
//document.body might be $('#main-container') or whatever
$(document.body).html(orderView.el);
}
I think that's the textbook pattern. Again, the issue of who triggers the fetching of data and rerendering after it arrives is tricky. I think it's best if the view knows how to render a "loading" version of itself until the model has fetched data, and then when the model fires a change event after fetch completes, the view rerenders itself with the loaded model data. However, some people might put that logic elsewhere. This article on building the next soundcloud I think represents many very good "state of the art" backbone patterns, including how they handle unfetched models.
In general, you can code things with callbacks or events as you prefer. However, a good rule of thumb is to ask yourself some questions:
Is more than one independent logical piece of work going to respond to this event?
Do I need to decouple the source of this event from the things that happen in response to it?
If both of those are "yes", then events should be a good fit. If both are "no", than straightforward function logic is a better fit. In the case of "navigating to this URL triggers this view", generally the answer to both questions is "no", so you can just code that logic into the router's route handler method and be done with it.
I'd use second scenario. Don't see any benefits of using first approach. It would make more sence this way (but still arguable):
/* ... */
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
vent.trigger('order:show', orderId);
}
/* ... */
vent.on('order:show', function(orderId) {
var viewOrderView = new ViewOrderView();
viewOrderView.render();
});
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function (options) {
this.orderId = options.orderId;
},
render: function () {
var template = viewOrderTemplate({
id: this.orderId
});
this.$el.html(template);
return this;
}
});
This way at least you'd be able to trigger route action without updating a url. But same effect might be achieved using Backbone.router.viewOrder(1) probably. Events are pretty powerful, but i wouldn't use them if i don't really need.

Resources