I have a collection that I'm appending to. On my app, I destroy all the models when I log out - with this piece of code :
logout: function(event) {
$.post('./logout');
App.Contacts.each(function (contact){
console.log(contact);
contact.destroy();
});
}
The problem is that not all the models are being deleted. If i press the logout button a couple more time to trigger this function, they eventually end up getting deleted.
The console complains that they are undefined - so it doesn't have a handle to it.
What could possibly be going on?
Why don't you use reset. Calling collection.reset() without passing any models as arguments will empty the entire collection.
Try :
logout: function(event) {
$.post('./logout');
App.Contacts.reset();
}
If you want to destroy all of them in order to fire destroy or remove events (which you won't get from reset), you can try using the invoke method.
For example,
logout: function(event) {
$.post('./logout');
App.Contacts.invoke('destroy');
}
In your code, every time you destroy a model, the length of collection will change, that may casue some problems, so if you want to handle it properly, use:
App.Contacts.reset();
Just as what Niranjan Borawake said.
Related
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 currently started using Firebase as my backend solution for persistance.
I found it easy to create new objects and persist it to Firebase with Backfire with a simple
collection.add(obj)
The issue comes when I try to get a Firebase collection from the server.
For example when i try
console.log(collection);
I get this output:
=> {length: 0, models: Array[0], _byId: Object, _events: Object, constructor: function…}
Which result in an empty models array
console.log(collection.models);
=> []
After some searching, I figured out that Backbone Collections aren't yet loaded at the time I try to log it to the console (see this previous question).
I also tried using
Backbone.Collection.extend({
model: Todo,
firebase: new Backbone.Firebase("https://<your-namespace>.firebaseio.com")
});
To explicitly call fetch from the server and use success callback with no success either.
My question is: How can I get the Firebase Collection and populate the DOM from it?
When you call Backbone.Firebase.Collection.add, it does not get added to the collection synchronously. Rather, it sends the request to Firebase and then waits for the return event. See the code here
Thus, if you immediately try to read the collection, you will see zero elements. However, if you try something like this:
collection.once('add', function() { console.log(collection.length); });
You'll see the element you have added.
Remember that we're dealing with real-time data here, so when you want to populate the DOM, you shouldn't think in terms of a single transaction, but instead rely on events and take everything as you get it (in real time).
So to populate the DOM, do something like this in your view:
Backbone.View.extend({
render: function() {
this.listenTo(this.collection, 'add', this.rowAdded);
},
rowAdded: function(m) {
/* use `m` here to create your new DOM element */
}
});
Additionally, you'll probably want to check out a nice binding library like ModelBinder to help you deal with the constantly changing DOM, so you don't have to re-invent any wheels.
It seems you have to use a Backbone.Firebase.Collection and not a Backbone.Collection which will tell you that your calls to fetch or sync are silently ignored.
Also, Backbone.Firebase's got a read and a readall methods that should get you started. It seems Backbone.Firebase.Collection doesn't inherit this method, but I'm not sure though.
Edit:
As Kato stated in his comment, it seems you don't have to do anything. Just use Backbone.Backfire.Collection and Backbone.Backfire.Model.
Use case: A user creates a new task, which has to be sent upstream through an API. When that API returns success, I add it to the scope for display. I have that working fine with:
$http.post('some_url', newtask).success(function(data) {
$scope.tasks.push(data);
});
(newtask is a simple object defined earlier, not shown here).
The problem is, the API is quite slow (though reliable), whereas I want this to feel like a real-time app. So I'd like to push the new task to the $scope immediately, then replace it with the "real" one when the API returns success. So I prepend the above with:
$scope.tasks.push(newtask); // Add to UI immediately (provisionally)
What happens now is that the new task is added immediately, then a second copy of the task is added when the API returns. So what I'd like to do is remove the first copy as soon as the second copy is added.
I can't seem to find a find a way to do that. Or is my approach all wrong? (I admit, the approach does feel like a bit of a hack, so I'm open to suggestions).
In the success callback function, I suggest you compare the new task (coming back from the API) to the last task in your tasks array. If they match, you don't have to do anything. (If they don't match, you'll need to remove the last item and throw up some kind of error indication.)
$http.post('some_url', newtask).success(function(data) {
if($scope.tasks[$scope.tasks.length-1] === data) {
// do nothing, we already added it before
} else {
... error handling here
}
});
If the user can't add more than one task at a time (before the API returns), and you really want to remove then add, just use normal JavaScript operations on the array to remove the last item, then add the API value:
$http.post('some_url', newtask).success(function(data) {
$scope.tasks.pop();
$scope.tasks.push(data);
});
Angular should notice the model change and automatically update the view for you.
//Push the new task immediately
$scope.tasks.push(newtask);
//When API return check for error if error just pop out the added task
$http.post('some_url', newtask).success(function(data) {
if($scope.tasks[$scope.tasks.length-1] !== data) {
$scope.tasks.pop();
}
});
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/
So today I just came across the 'live()' function that binds any future and past elements to the whatever event you choose, such as 'onclick'.
Right now I'm having to set up buttons like the following each time I load a new button via ajax ...
$('a.btn.plus').button({icons:{primary:'ui-icon-plusthick'}});
$('a.btn.pencil').button({icons:{primary:'ui-icon ui-icon-pencil'}});
$('a.btn.bigx').button({icons:{primary:'ui-icon ui-icon-closethick'}});
So, instead of calling these lines each time I use ajax to add a new button, is there a similar way to tell JQuery to setup my buttons ANYTIME I add new ones?
Thanks for any help!
Mmh not really. But there is the function .ajaxSuccess() which is triggered whenever an Ajax call is successful. So you could do:
$('body').ajaxSuccess(function() {
$('a.btn.plus').button({icons:{primary:'ui-icon-plusthick'}});
$('a.btn.pencil').button({icons:{primary:'ui-icon ui-icon-pencil'}});
$('a.btn.bigx').button({icons:{primary:'ui-icon ui-icon-closethick'}});
});
But this will run on any links with the classes, not only on the new ones. But if you append them on a time (i.e. not multiple a.btn.plus at once) you might be able to use the :last selector (a.btn.plus:last).
You can also create a function and just that from your callback functions:
function links() {
$('a.btn.plus').button({icons:{primary:'ui-icon-plusthick'}});
$('a.btn.pencil').button({icons:{primary:'ui-icon ui-icon-pencil'}});
$('a.btn.bigx').button({icons:{primary:'ui-icon ui-icon-closethick'}});
}
and in the Ajax call:
$.ajax({
//...
success: function(msg){
links();
}
});
This way you can pass the parent element to the function in order to find the link only inside this element (so the code would only work on the new links).
A last option would be generate a custom event but in the end this would be similar to just doing a function call in your case so you gain not much.
You can use delegate in your success function too
$("body").delegate("a.btn", "hover", function(){
$(this).toggleClass("hover");
});
There is a Jquery Plugin called livequery which covers your requirements.
I like to think of this plugin as Jquery .live() but without the need for an event ('click') etc.
You can find more info here//
Jquery - Live Query Plugin