remove orphaned child views in backbone - backbone.js

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.

Related

How can I unbind subviews after navigating to a different view in Backbone.js?

I am new to backbone, and I'm trying to fix a memory leak issue. The project I'm working on has a collection of models that is represented by a list. Each model has a submit button. The button navigates the user to a view for the corresponding model that either lets them assign the model's data to a user in the database, or return to the list view. Everything I described so far works as intended, but when the user returns to the list after navigating from a model view and chooses a different model to submit instead, then the old one will be submitted in addition to the one in the current view.
From what I've read, I know this is caused by the old view listening to events that are still bound to it, and I've looked into the various backbone functions (remove, unbind, listenTo, etc) that can be used to deal with these views. However, I've tried the functions in the view file where I believe the leak is coming from and it still doesn't seem to kill the old views. I'm also using a function in the router that removes views before loading another, but it doesn't seem to remove the subviews for the models that were navigated to in the list.
The initialize function, return function, and events for the view generating leaks
initialize: function (options) {
if (!(options && options.model)) {
throw new Error("No item specified for model");
}
if (!options.bus) {
throw new Error("No bus specified");
}
if (!options.router) {
throw new Error("No router specified");
}
this.bus = options.bus;
this.router = options.router;
this.listenTo(this.model, "returnToList", this.unbind()); //using unbind here after the return button is clicked
},
events: {
"click #submit": "submit",
"click #back": "returnToList"
},
//this function will call a function in the router that creates a view for the collection and then calls the loadView function to remove the old view.
returnToList: function () {
this.router.navigate("exceptionIndex", { trigger: true });
}
This is the function used in the router to remove old views
loadView: function (view) {
if (this._currentView) {
this._currentView.remove();
}
$("#MainContainer").html(view.render().$el);
this._currentView = view;
}
The model view should only submit the model that is being referenced for the view, even if the user previously navigated from other model views.
The console produces no errors when this issue occurs.

Repeatedly creating and destroying views in Backbone.js or Marionette.js without creating a "memory leak"

I suspect that the way I am handling views in backbone.js is flawed in such a way that it is creating a "memory leak".
There is a view that is constantly being overwritten with another copy of itself. The new copy is linked to a different model.
I am creating and adding the view to it's parent view by setting the el option when creating the child view.
The strange thing that is happening is that even though a new view is being rendered over top of the old view, when I click the "button" an alert pops up for every childView that was every rendered, even though the button they were listening to should be gone, they respond to the new button.
I've implemented a quick fix by calling a function on the old view to stop listening to events before the new view is added. But that this problem exists at all tells me that all of the old views are hanging around and will slow the application over time if the user does not refresh the page often enough.
var parent = new (Backbone.Marionette.ItemView.extend({
ui:{
child_container: '#child-container'
},
onRender: function(){
// Listen to outside event
...
}
on_Outside_Event: function(new_model){
// Quick fix prevents multiple alerts popping up for every child view when "button" is pressed
this.child_view.destroy_view();
// New child view is created and rendered on top of the one that was there before
this.child_view = childView({
el: this.ui.child_container, // <-- Is this the problem?
model: new_model
})
this.child_view.render();
}
}))();
var childView = Backbone.Marionette.ItemView.extend({
events:{
'click button': 'on_click_button'
},
on_click_button: function(){
// Alert pops up once for every view that was ever displayed.
alert('Button clicked');
},
// QUICK FIX
destroy_view: function(){
this.undelegateEvents();
}
})
In case this is helpful, here is a screen shot of the actual application. A calendar of appointments is on the right. The problem child view - a view of the individual appointment that the user wants to see is on the left.
When I click the "Cancel appointment" button, that function gets called for every appointment that was every displayed in that area, even though I am listening to the event using: events:{ 'click #cancel-button': 'on_button_click'}
None of the other buttons, interactions, and other controls have this same issue, I assume because all the others actually live views that are children of the child appointment view and not in the child appointment view itself
A possible fix?
Did a little searching around, does this fix look adequate?
Normally, I think the removeData().unbind(); and remove() functions are called directly on this.$el, but this did not work here, I think because I added the child view using the el option when it was created (el: this.ui.child_container)
var childView = Backbone.Marionette.ItemView.extend({
...
// REAL FIX
destroy_view: function(){
this.undelegateEvents();
this.$el.children().removeData().unbind();
this.$el.children().remove();
}
I think you should make your parent view a LayoutView (that's just an ItemView with added functionality to handle regions iirc), have a region defined for where you want this child view to appear, and then do:
Parent = Backbone.Marionette.LayoutView.extend
regions:
child: "#child-container"
on_Outside_Event: ->
childView = new ChildView(...)
#getRegion("child").show(childView)
(sorry I used coffeescript, it's faster to write, but you can translate easily).
Marionette will handle everything: closing your old child view, unbinding events, etc.

Collection create function firing add event too quickly

(Using Backbone 0.9.10)
I have a list view of sorts where the user can click a button to show a modal view. The list view has a counter that shows the amount of items contained in the list. Clicking a button in the modal view executes create on a collection that is passed into both views. This fires the add event in the list view, which in turn runs this function:
renderCount: function () {
// Container for model count
var $num = this.$el.find('.num');
if ($num.length > 0) {
// 'count' is a value returned from a service that
// always contains the total amount of models in the
// collection. This is necessary as I use a form of
// pagination. It's essentially the same as
// this.collection.length had it returned all models
// at once.
$num.html(this.collection.count);
}
}
However, add seems to be fired immediately (as it should be, according to the docs), before the collection has a chance to update the model count. I looked into sync but it didn't seem to do much of anything.
How can I make sure the collection is updated before calling renderCount?
Here's the list view initialize function, for reference:
initialize: function (options) {
this.collection = options.collection;
this.listenTo(this.collection, 'add remove reset', this.renderCount);
this.render();
}
EDIT:
Turns out I was forgetting to refetch the collection on success in the modal view.
$num.html(this.collection.count);
shoule be:
$num.html(this.collection.size());
Backbone.Collection uses methods imported from underscore, here is list: http://backbonejs.org/#Collection-Underscore-Methods
Turns out I was forgetting to refetch the collection on success in the modal view.

Initializing subviews

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?

View + Sub Views in backbone/backbone-relational app

I've been learning a lot about backbone, backbone-relational and building web apps from reading through stackexchange - so first a thank you to the community.
Now, I am stuck at trying to understand this current issue involving nested models and sub views in what seems to me to be is a pretty common usecase.
I am trying to extend this tutorial to learn about backbone-relational, views/subviews and event handling by keepin track of "CheckIns" for each wine.
I have extended the server side to return appropriate JSON and backbone-relational model for checkIns like so:
window.CheckInModel = Backbone.RelationalModel.extend({
defaults:{
"_id":null,
"Did":"true",
"dateOf":"",
}
});
window.CheckInCollection = Backbone.Collection.extend({
model : CheckInModel
});
And the Wine Model like so:
relations: [{
type: Backbone.HasMany,
key:'CheckIn',
relatedModel: 'CheckInModel',
collectionType: CheckInCollection,
}]
I've created a CheckInListView and CheckInItemView (same as WineListView and WineListItemView) and use the WineView Render function to render the CheckIns like so:
render:function (eventName) {
console.log("wine View Render");
$(this.el).html(this.template(this.model.toJSON()));
this.myCheckInListView = new CheckInListView({model: this.model.attributes.CheckIn});
this.$el.append(this.myCheckInListView.render().el);
return this;
},
I've also created a new function within wineview that creates a checkin and associated with the given event:
logcheckin: function (event){
var todate = new Date();
newCheckIn = new CheckInModel ({'Did':"true", 'dateOf': todate.toISOString()});
console.log ("Logcheckin - About to push newCheckIn onto Model.");
this.model.attributes.CheckIn.push (newCheckIn);
console.log ("Just pushed newCheckIn onto Model.");
this.saveWine();
}
Ok - if you haven't TL/DRed yet - This all seems to work fine from a UI perspective -ie. everything renders correctly and saves to the Db.
But I notice in the console that when I push a new CheckIn (between the console.logs above) the CheckInListView's Add binding gets called multiple times for wach button press - Which makes me think something is wrong with how I'm doing views or that I am not understanding something fundamental about event propagation.
Why is this happening ? Is it expected behavior ? Am I approaching what I am trying to do correctly ?
Thanks for reading if not your help.
==
Here are the relevant parts of the CheckinListView and CheckInList Item views that are bound to the add (and other) events.
window.CheckInListView = Backbone.View.extend({
initialize:function () {
this.model.bind("reset", this.render, this);
this.model.bind("change", this.render, this);
var self = this;
this.model.bind("add", function (CheckIn) {
console.log ("Adding to CheckInListView - a CheckIn List Item", CheckIn);
self.$el.append(new CheckInListItemView({model:CheckIn}).render().el);
});
},
close:function () {
$(this.el).unbind();
$(this.el).remove();
}
});
window.CheckInListItemView = Backbone.View.extend({
initialize:function () {
this.model.bind("change", this.render, this);
this.model.bind("destroy", this.close, this);
},
});
==============================================
The comment about event binding and closing views were the right hints for debugging this.
1) I was not closing and unbinding the nested views properly which left some ghost event consumers even though there was nothing in the DOM
2) You only need to bind events if we want to do something only in the subview.
For Example - If I have checkbox in a subview I can bind a subview change event in the main view and handle the event there since the mainview has model there anyway. I don't know if this is the "right" way but it works for what I need to do. (mm.. spaghetti code tastes so good)
3) Struggling with this helped me think through the UX some more and helped me simplify UI.
4) I was trying to "save" calls to the server by nesting all the data into on JSON call. And if I were to re-do this - I would not nest the data at all but handle it on the back end by associating the wine ID with checkIn ID and then having a separate collection that gets populated with the collection once a task is selected - I thought this would not be a the preferred way but it seems to be the way that a lot of people.
Still welcome any thoughts on the "right" way questions above or if anyone can point to a tutorial that goes beyond the "simple backbone app"
I'm not sure about everything that's happening, but, I've run into the problem of events firing multiple times before. If you're rendering multiple models using the same view, there's a chance that they're all being bound to the same event.
Perhaps this answer might apply:
Cleaning views with backbone.js?
If not, you should respond to Edward M Smith's comment and show how your events are being bound.

Resources