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.
Related
Usually I have a View on which I set listeners to model-changes, like so (conflated):
var jsonModel = {bla: 'interesting stuff'}; //some model in json, probably rendered in a dom-element and passed from the server to the client
var someModelType = Backbone.RelationalModel.extend({
bla: "String"
});
var someModelInstance = new someModelType(jsonModel);
var someViewType = Backbone.View.extend({
initialize: function(){
this.listenTo(this.model,'change:bla', function(model){
//update view to sync with model change here
}
}
});
var someViewInstance = new someViewType({
model: someModelInstance
});
I'm looking for the preferred / backbone - way of bootstrapping the view, i.e: I want my view (form fields or what have you) to be synced with the model on creation. With the above this is impossible since the model is created before the view is attached, which results in model-updates/changes to be fired before listeners from the view were initialized.
Of course I could code some custom bootstapping logic which would call the listener-functions manually, but since this must be such a common problem, I'm looking on some best practice advice, or even better, a Backbone-switch I need to set to get this to work.
This will sound silly, but what I usually do is do a render() after the view creation.
For me, the whole point of the change event is to notify of a change. The fact that it's triggered also on creation (as a "changed compared to the default") is more a side effect than anything deep...
In that sense, when I create the views, and give it an existing model, I expect the model to be somewhat ready and so I should be able to do a render() right away. So my bootstrapping code would be more like:
var someViewInstance = new someViewType({
model: someModelInstance
});
someViewInstance.render()
Or do it in the initialize() after bindings if you feel adventurous (I personally don't like that because when dealing with subviews, it's hard to know exactly when you want the render to happen, especially when dependent on libraries who need the DOM to be ready, and can't work on detached nodes).
So to sum up, although your problem is common, to me, it doesn't need any kind of complex solution: you have a render() function to transform your model into DOM-stuff, your model may or may not be ready but it exists, so you pass it to the view, and you do the first rendering after creating the view, manually, because you know it needs to happen. Then you bind to changes, in case your model is updated, to update the DOM accordingly.
side note: in case of a collection of models, when the Model isn't ready at ALL, then it probably shouldn't even have an instance, and you should let the Collection give you some add/remove events for when it pops into existence.
I'm quite confused with backbone's view rendering. I need your help regarding this.
For example, I've a Album view. It's render method renders each Track view. All the track specific events are binding in the Track view.
Now, Track view is listening to it's model. For example:
this.model.on('destroy', this.destroy, this);
destroy: function(model){
this.close();
},
so when a track is destroyed, the view is removed from UI which is fine. But problem is with sync. The view is removed from the UI not from server. If sync is successful, nothing to do with the view.
However, if the sync is not successful, I want to undo this view change (restore the track markup where it was before I destroyed it).
Can anyone please tell how can I do this?
FYI,
earlier I've altered the UI after getting success from server. But in more than 98% cases we get success, we decided to change the UI immediately and restore when there is an error.
There's a great plugin for dealing with this: https://github.com/derickbailey/backbone.memento
I call view.remove() in the success callback of model.destroy, I think its easier to read the code and understand what happens that way.
But if you really want to restore the view on sync.error I guess you could call a restore method in an error callback from your destroy call. But it feels like more work to restore it than to wait for the ack.
I am trying to test drive a view event using Jasmine and the problem is probably best explained via code.
The view looks like:
App.testView = Backbone.View.extend({
events: { 'click .overlay': 'myEvent' },
myEvent: function(e) {
console.log('hello world')
}
The test looks something like:
describe('myEvent', function() {
it('should do something', function() {
var view = new App.testView();
view.myEvent();
// assertion will follow
});
});
The problem is that the view.myEvent method is never called (nothing logs to the console). I was trying to avoid triggering from the DOM. Has anyone had similar problems?
(Like I commented in the question, your code looks fine and should work. Your problem is not in the code you posted. If you can expand your code samples and give more info, we can take another look at it. What follows is more general advice on testing Backbone views.)
Calling the event handler function like you do is a legitimate testing strategy, but it has a couple of shortcomings.
It doesn't test that the events are wired up correctly. What you're testing is that the callback does what it's supposed to, but it doesn't test that the action is actually triggered when your user interacts with the page.
If your event handler needs to reference the event argument or the test will not work.
I prefer to test my views all the way from the event:
var view = new View().render();
view.$('.overlay').click();
expect(...).toEqual(...);
Like you said, it's generally not advisable to manipulate DOM in your tests, so this way of testing views requires that view.render does not attach anything to the DOM.
The best way to achieve this is leave the DOM manipulation to the code that's responsible for initializing the view. If you don't set an el property to the view (either in the View.extend definition or in the view constructor), Backbone will create a new, detached DOM node as view.el. This element works just like an attached node - you can manipulate its contents and trigger events on it.
So instead of...
View.extend({el: '#container'});
...or...
new View({el:'#container'});
...you should initialize your views as follows:
var view = new View();
$("#container").html(view.render().el);
Defining your views like this has multiple benefits:
Enables testing views fully without attaching them to DOM.
The views become reusable, you can create multiple instances and render them to different elements.
If your render method does some complicated DOM manipulation, it's faster to perform it on an detached node.
From a responsibility point of view you could argue that a view shouldn't know where it's placed, in the same way a model should not know what collection it should be added to. This enforces better design of view composition.
IMHO, this view rendering pattern is a general best practice, not just a testing-related special case.
ive got a problem which im not sure how to bite.
Ive got a search form with a lots of filters. I store all current fitlers
in global namespage ie: window.NM.CurrentSearchParams = {} and i update the hash from filters. Each time the hash is updated, updating uri event is fired to
replate the window.localtion.search with current params. Everything is workin here fine.
Now ive got a problem after entering page to deserialize window.location.search and
update CurrentSearchParams with parameters. Thats fine also, but i would like
to forms to repopulate based on those params.
Ive got a little mess in code and not sure the best way to do it.
How to bind form population in different views to the parameters?
make the CurrentSearchParams a Backbone.Model so you can subscribe to change events on it.
NM.CurrentSearchParams = new Backbone.Model();
MyView = Backbone.View.extend({
initialize: function(){
NM.CurrentSearchParams.on("change", this.doStuff, this);
},
doStuff: function(){
// do stuff here
},
close: function(){
NM.CurrentSearchParams.off("change", this.doStuff, this);
}
});
Note the "close" method that I added. This is very important and you need to call this when your view is done and ready to be closed, or you will end up with a lot of memory leaks and events triggering on zombie view instances: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
We have some legacy non-backbone code in our web application. Although we attach views to existing DOM elements there is still some yet-to-be-refactored code that deletes certain DOM elements i.e. the delete call doesn't go through the view but is more like a jQuery call $('#domID').remove();
I have a nagging feeling that the backbone view probably hangs around as a zombie, but I don't have a way to see it? Is this harmful? Should we make it priority to refactor and have all the deletes go via the view and call view.remove() and view.unbind() for proper deletion?
Would the view be garbage collected if the DOM node is deleted independently? I guess if not if it's bound to some event, but what if it isn't?
The view will only linger on if there is a reference to it somewhere. There are four sources of stray references to consider:
Bindings to models and collections, i.e. this.collection.on('reset', this.render) and such.
Bindings to DOM objects through the view's events.
Bindings to DOM objects through direct $(...).on(...) calls.
Plain old variable references such as this.current_view = new V(...).
(1) is normally handled by the view's remove method and you have to call remove yourself, there's nothing in Backbone or jQuery that can do this for you. For example: http://jsfiddle.net/ambiguous/e574Z/
(2) is easy. Backbone views use a single delegate call to bind the view's events to the view's el. So, if you remove the view's el through a simple $(x).remove() then the event reference goes away. However, if you're attaching different views to the same el, you'll need to call undelegateEvents to detach the delegate; this would normally be done in a remove method:
remove: function() {
this.undelegateEvents();
return this;
}
but, again, you have to call remove yourself somewhere.
(3) is rare but sometimes necessary in the case of window scroll events, body click events for dialogs, and things like that. Of course, you have to clean these up yourself as Backbone can't know what you're doing behind its back and the elements that you're binding to would be outside of the view's el (or you'd be in (2)). Where would you clean these up? The remove method of course.
(4) is, as always, up to you. Usually, this sort of thing is handled like this:
if(this.current_view)
this.current_view.remove();
this.current_view = null;
Yes, there's remove again.
So, if all you have are things like (2), then $('#domID').remove(); will be fine and shouldn't leave any zombies; in fact, the default remove implementation is just this.$el.remove() and the documentation says as much:
remove view.remove()
Convenience function for removing the view from the DOM. Equivalent to calling $(view.el).remove();
However, you probably have some things like (1) involved as well so adding/updating all your remove methods and calling view.remove() to remove views would be a good idea.