I am trying to remove all child views in Backbone by modifying the Backbone.View.prototype like this:
_.extend(Backbone.View.prototype, {
childViews:[],
close: function(){
this.remove();
this.unbind();
console.log(this.childViews.length);
_.each(this.childViews,function(childview){
childview.close();
},this);
}
});
So when I create a child view, I push to childViews. When I close I expect it to also call close on the childViews. If it has nothing in childViews, then I expect the chain of close to stop.
fiddle
What ends up happening is some sort of infinite loop. I can't figure out why it is behaving like this. Is it a problem with _.each's [context]? Can someone explain what I am doing wrong and how to fix this?
Your problem is that you've added childViews: [] to the prototype. That means that every single view instance will share exactly the same childViews array. Once you have any child views anywhere, you will end up with this.childViews containing this and there's your infinite loop.
Instead, you should create childViews in the view's initialize:
initialize: function() {
this.childViews = [ ];
//...
}
You might have to update your close method to allow for an undefined this.childViews as well.
Updated fiddle: http://jsfiddle.net/ambiguous/f2ykv/
As a rule of thumb, you almost never want any mutable properties (such as arrays or objects) attached to the prototype, you almost always want those properties assigned per-instance.
Related
I have a Backbone view whose render method creates and caches some JavaScript objects then renders them out to SVG nodes in the DOM. So (very simplistically), I'm doing something like this:
render: function() {
// Create objects and cache them to the view...
this.someObjects = [obj1, obj2, ...];
// Render objects...
this.someObjects.forEach(function(obj) {
obj.render(); // Each object knows how to render itself...
})
return this;
}
My view events hash is returned from a function, and contains event handlers which close over certain variables which depend on the objects cached when rendering. So (again simplistically, just for illustration purposes), I'm doing something like this:
events: function() {
var getRenderedObjects = function() {
return this.someObjects;
};
var renderedObjects = getRenderedObjects();
return {
'click #someDiv .someClass' : function() {
console.log(renderedObjects);
}
}
}
The problem is that Backbone sets up the bindings for the events hash before the view initialization function runs, which is where I need to do an explicit render. So this.someObjects will be undefined. Before moving to Backbone all my event handlers were defined and bound after rendering the view, which seemed natural anyway. I have found a workaround, which is to set the events hash manually on initialization, after rendering, and then call delegateEvents() to ensure they're bound. This works, although I found that within the handlers this no longer refers to the view object, but rather to the global window object again. It all seems rather cumbersome and a bit hackish, however.
I guess I could leave renderedObjects as null and set it conditionally in the handler which requires it (so if it's not set, set it, else do nothing). That seems ugly, though. Is there a better way?
I'm still confused by this. The common paradigm I see emerge is...
view.render()
With render set to
function () { this.$el.html( this.template() ) }
How does this behavior mesh with remove? This renders the view.. but then logically you'll call .remove() to unbind the events and ditch it.. If you use that though, you've ditched the parent container ($el), and can't re-render. The docs need to be explicit then that the convention is to not this.$el.html( this.template() ), but to render the template by calling .html() on some non-el parent element. You can either call .render() using the views own $el and also use .remove().
How does these two work together?
The best practice I see is slightly different. Leave the el property off entirely -- let backbone create its own element. The render method should look like this:
render: function() {
this.$el.html(this.template(some_data_from_somewhere));
return this;
}
and then you call render from the router, thusly:
some_container.append(view.render().el)
If you do it this way, then the default remove implementation:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
starts to make more sense. The view created and has sole control of its el so it makes perfect sense for the view to destroy the el when it is being destroyed. The view's owner creates the view and adds it to the page; then later, the view's owner destroys the view (by calling remove) when the owner is done with it. This approach makes the view nicely self-contained.
Of course, if your view is expecting to bind to an existing el or have someone give it an el when it is created, then you're going to have to provide a custom remove to unbind DOM events (by calling this.undelegateEvents() presumably), remove the contents of this.el (but not el itself), etc. Similarly, if your view has child views then you'd provide a remove that would call remove on all the children before cleaning itself up.
I don't think I fully understand your questions; but I think if you look at the sections titled:
Decouple Views from other DOM elements
and
Cleaning Up: Avoiding Memory Leaks and Ghost Views
form the following link, then you might just find the answer that you were looking for.
http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/
It was only until recently that I started adding my view to the DOM from outside of the view itself meaning I don't pass in an el: when creating the view. I just have he view render the DOM element from within the view to memory and call render().$el from the calling code to render the view just like the article states.
Note: we are using backbone 1.0.0
I am relatively new to Backbone, and was going to through some of the code a ex co-worker wrote. Rather than copy pasting stuff blindly, I wanted to understand how he did things, and that's when I started wondering about the best way to handle zombie views.
var view = new editItemView({ model: this.model });
this.ui.editItemPopup.html(view.render().el).modal({ modalOverflow: true });
This creates an instance of view and pops it up in a boostrap modal. The model has Save Changes, Cancel & Delete buttons. We will look at the clean work that is done on Save changes and delete.
onDelete: function() {
this.stopListening(this.model);
this.$el.parent().modal('hide');
this.$el.remove();
},
onApplyChangesClick: function () {
this.stopListening(this.model);
this.close();
},
close: function () {
this.$el.parent().modal('hide');
}
As far as I can tell, this code won't discard the view. And if I were to add another listener to the aforementioned view
this.listenTo(this.model.AnotherItem, 'change', this.doSomething);
and then trigger the change event on this.model.AnotherItem, this.doSomething will still fire. Correct?
I did some reading on Zombie views prior to posting this question. http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
And based on that article wouldn't I be better off if I just did
onDelete: function() {
this.close()
},
onApplyChangesClick: function () {
this.close();
},
close: function () {
this.$el.parent().modal('hide');
this.remove();
}
his.remove() will automatically call stopListening and also remove the dom element(Same as this.$el.remove)
The article that I posted also uses this.unbind()
this.unbind()will unbind any events that our view triggers directly – that is, anytime we may have calledthis.trigger(…)` from within our view, in order to have our view raise an event.
Is that still necessary in Backbone 1.0.0 (or latest version)? The article is 3 years old, so I was wondering and I couldn't find any mention of view.unbind in backbone documentation. The documentation mentions that unbind is an alias of off. So should I be doing
this.remove();
this.off();
Ok, first off let me state the obvious: a few zombie views here or there are not going to cause you any problems. All any given zombie view will do is eat up a small amount of memory and then go away when the user hits refresh or navigates away. So, if you're a little sloppy about cleaning up your references in general things will still work fine. Where you will run in to problems is when you have a lot of zombie views, say because you rendered a 20x100 table where every cell has its own View.
Now, to truly understand how to avoid zombie views you have to understand how memory works in Javascript. I encourage you to read more about that elsewhere, but here's the cliff notes version: anything you "stop using" will get cleaned up by the browser's garbage collector, and since the garbage collector can't tell exactly when you "stop using" something it actually goes by whether or not that thing has any references to it on other objects.
This is where event bindings come in to play, because they can create references that prevent a view from being garbage collected. One of the features of Backbone is that it handles cleaning up these bindings if they are made as part of a Backbone.View's initialization (ie. the events you put in an events property of the View class). So if you remove a View's element from the page, it will get garbage collected ...
... unless it has some other reference to it, like another object that uses it, or an event binding that you created using jQuery. So, as long as your Views don't have any other references you are correct: simply removing the element will be enough. But if you do have any other references, you will need to clean them up or else the View won't get garbage collected and will become a zombie view.
I have a backbone view that contains project data for various research projects.
In this view, I have a button, that when clicked, it executes a method called 'toggleChildView'. This method inserts a child view into the main page.
In the child view, I'm listening for an event where the user clicks anywhere on the page except for the element that contains the research child review.
The problem is, if I close the child view, the child view is actually still hanging around somewhere because the event is still firing, and will fire multiple times if I had opened and closed the child view.
So for example, if I opened and closed the childview 5 times, after the final close, the event will still fire 5 times.
But it shouldn't fire at all if closed, and only once when opened.
I think my question would best be phrased like this:
Is there anyway to get rid of "orphaned" child views and ensure that only one child view is open at a time?
thanks!
Parent View:
toggleChildView: function (e) {
this.$('.displayresearchdata').toggle();
this.$('.editdata').toggle();
//initialize the research view into our main view
var researchView = new researchView({ parent: this });
self.$('.research').append(researchView.render().$el);
},
saveresearchdata: function (e) {
this.model.save('researchData', this.$(".researchData").html());
},
Child render method:
initialize: function (options) {
this.parent = options.parent;
},
render: function () {
var self = this;
this.$el.append(this.template());
$("body").on("click", function (event) {
if (!$(event.target).closest('.editdata').length) {
if (self.parent.$('.editdata').is(":visible")) {
self.parent.saveresearchdata();
}
}
});
return this;
},
As #mu is too short points out, you need to explictly remove() any view you add.
If that view adds some custom event listeners, you should remove them too. If you use the view.listenTo(target, "eventname", this.functionName) flavor of event listening, those event handlers will be removed automatically when you call view.remove() because of the stopListening() method being called.
In your code, the problem is that you're not keeping a reference to the child view(s) you're adding, so you can't call remove on it. Keep a internal reference from the parent to the child like:
//initialize the research view into our main view
if(this._researchView) {
this._researchView.remove();
}
this._researchView = new researchView(...)
this.$(".research").empty().append(this._researchView.render().$el);
Take note of the use of empty before appending, if you don't want to have many researchViews added, only one at a time. If you indeed want many views, then you can remove that, and keep the internal reference as an array.
Handling so-called 'zombie' views is one of the trickiest parts of using Backbone, and if you have a lot of sub-views, it can become a real problem if you do not manage the views correctly. The seminal post on the subject is this one by Derrik Bailey although note that some of the methods he references, such as bind are now deprecated in favor of listenTo
#CharlieBrown's answer will do trick. However, if you plan on creating other views and/or subviews, here's one way you can set things up on a larger scale:
1) Create a Baseview, from which all other views will be extended.
var BaseView = Backbone.View.extend({
//typical initialize and render functions
//...
//Create the close method
close: function () {
if (this.onClose) this.onClose(); //looks for an onClose method in your subviews
this.undelegateEvents();
this.$el.off();
this.$el.children().remove();
this.$el.empty();
this.stopListening();
}
});
2) Now in your Backbone router you can create a trackView function which will call the the close() method from the base view
//within router
trackView: function (next) {
console.log('now closing ' + next.cid);
if (this.current) this.current.close();
this.current = next;
},
3) All other views in the router should now be called from within trackview like this:
//within router
someView: function () {
console.log('loading create user page');
this.trackView(new someView()); //use trackView from step 2
},
4) Finally, within any sub-views, be sure to add an 'onClose()' method where you can close down any potential zombies using the close method inherited from the Baseview:
//inside a view
onClose: function() {
if (this.couldBeAZombieView) this.couldBeAZombieView.close();
}
Now you are setup for a more complex site. There are other ways to set this up, but this is the one I'm familiar with.
I have been looking at some code for a while now and can't decide on the best practice to apply in this situation:
Let's say that we have a view that has n-subviews. I have come across two practices for initializing them
1 Inside intitialize
initialize: function() {
this.subViews = [];
this.subViewModelCollection.each(function(model) {
var view = new SubView({model: model});
this.subViews.push(view);
this.$el.append(view.el);
}, this);
},
render: function() {
_.invoke(this.subViews, 'render');
}
2 Inside render
initialize: function() {
... // render handles the subviews
},
render: function() {
this.subViews = [];
this.subViewModelCollection.each(function(model) {
var view = new SubView({model: model}).render(); // render optional
this.subViews.push(view);
this.$el.append(view.el);
}, this);
}
now these are just crude samples, but they demonstrate my dilemma. Should the initialize or the render -function be responsible for initializing the subviews? Personally I have been a strong advocate for the latter version, but some code I saw made me sway towards the former.
So, WHERE do you initialize your subviews, WHY do you do it there and WHY is it better than the other option?
You should maximize the amount of work you do in initialize, as that will only be done once, and minimize the amount of work you do in your render function which may typically be called many times (for instance in a single page application or responsive webpage).
If you know your render method for your subviews will never change the generated html, then you can probably also call the render method for the subviews in the initialize method as well, and simply appending the rendered element in the "main view".
If the render methods for both your subviews and main view is only called once anyway (as part of loading a page or whatever) it probably does not matter how you do it, but generally minimizing the amount of work to be done in the render functions is probably good advice.
I would put the actual sub view rendering functionality into a new function completely, say renderSubViews().
This will allow for greater flexibility.
You can call this function from initialize or render, and change it easily as your application grows/changes.
You can bind this new function to events, i.e. the reset event for the view.
This is more logical. Does it make sense to auto-render sub-views when you initialize or render the main view? Should one be directly tied to the other?