How do I get a view to refresh after I delete a model from a collection? - backbone.js

I have a view that is a list of phones. In the list, you can add or delete a phone. Here, for example, is the function called when a delete phone form is clicked on. The problem is that I want to navigate to the updated list once the phone is deleted. The way that I am using Backbone, that will mean refetching the collection of phones from the server, but I guess that's okay, as long as the list is updated. With the function below, the list is not refreshed.
deletePhone: function (ev) {
var that = this;
ev.preventDefault();
var phoneDetails = $(ev.currentTarget).serializeObject();
var phone = new P.phone();
phone.set({id: phoneDetails['phone-to-delete']});
phone.destroy({
success: function(model, response, options) {
Backbone.history.navigate('phones', {trigger: true});
},
error: function(model, response, options) {
console.log("In error: ");
}
});
},
So how do I navigate to the updated list?

Once you call delete on a model within a collection and that model is deleted successfully on the server, that model will trigger a 'destroy' event that will bubble up to the collection. The view just meeds to be told to listen to a destroy event on the collection, and then re-render once this happens. To do this, put this line of code in the initialization function of the view:
this.listenTo(this.collection, 'destroy', this.render);
This code will call the render function once a model in the collection has been deleted. Notice that it is not necessary to get the updated list from the server, as backbone has already removed it from the collection on the front end. Of course, if there is a possibility that the list has been changed by other clients, then grabbing it from the server again may be needed.
This may be better than placing a render call in the remove function itself the cover that case that a model is removed from the collection by other means.

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.

Destroying model instance does not remove it from views/storage

I am using Backbone, Marionette, and Backbone.localStorage to persist a list of items. I have a remove button on the ItemViews which fires the model's destroy method. This removes the model from the collection successfully and fires the remove event.
However, the CollectionView does not remove its ItemView automatically (as I think? it should), nor does it remove the model from local storage, so on reloading the page, it's back in the list.
Edit:
The model is removed by this method on the view:
confirmRemove : function() {
this._setRemoveState(false);
this.model.destroy({success: function() { console.log('model destroyed!');}});
return false;
}
The success callback does fire.
The problems:
I overwrote the ItemView's remove method unintentionally, so it wasn't removed from the DOM.
destroy only runs its sync actions on models with ids, assuming that only models with ids are persisted to the server. However, while Backbone.localStorage creates internal ids, it does not persist them back to the models, so there was no id property and Backbone skipped removing it from the server.

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.

Backbone - Using fetched data inside the view

I have fatched the data to Collection variable, but I am not sure what should I do next. How to use this data and to fill the template with it? Here is my code from the render function inside the View.
Collection.url = "../data";
Collection.fetch();
var compiled = _.template(self.data);
self.$el.prepend(compiled(/*MY JSON SHOULD GO HERE*/));
I am a newbie to backbone, so every help is appreaciated.
Here is a Collection definition:
var MainCollection = Backbone.Collection.extend({
model: MainModel,
//localStorage: new Backbone.LocalStorage("kitchen"),
initialize: function (models,options) { }
}), Collection = new MainCollection;
Here is a log of Collection and Collection coverted to JSON:
Assuming Collection is your collection's name (that's pretty confusing I have to say), this is what you're looking for:
self.$el.prepend(compiled(Collection.toJSON()));
Edit:
Don't forget you're fetching the data asynchronously. So when you're evaluating your template, the data hasn't come back yet, and your collection's still empty. Listen to the end of the request ('sync' event I think) or some other events so you know when the collection's populated OR use the success option of the fetch method to specify a callback :)
As for your logs. When you log an object, it will be automatically updated until you check the details. So you logged it while it was empty, but checked it after it was populated (a few ms afterwards).

Debugging Backbone.js: rendering after collection fetch()

I'm trying to do basic render() after fetch() on collection (Backbone 0.9.2):
var ProjectListView = Backbone.View.extend({
el: $('#container'),
initialize: function () {
this.collection = new ProjectsCollection();
this.collection.bind("change", _.bind(this.render, this));
this.collection.fetch({ success: function () { console.log("collection fetched"); } });
...
},
render: function () {
console.log("rendered");
...
Creating new View instance prints out:
collection fetched
So the render() never gets called after fetch(). What am I doing wrong here? There are no exceptions present.
Any tips how to debug these sort of things in backbone?
Ps.
It seems that this feature is poorly documented given the number of questions on SO.
From the fine manual:
fetch collection.fetch([options])
Fetch the default set of models for this collection from the server, resetting the collection when they arrive. [...] When the model data returns from the server, the collection will reset.
And what does reset do? reset does this:
reset collection.reset(models, [options])
[...] Use reset to replace a collection with a new list of models (or attribute hashes), triggering a single "reset" event at the end.
So fetch calls reset to update the collection's models and reset triggers a "reset" event, not a "change" event. None of the models have changed and a collection's "change" events come from its models.
You should have render bound to "reset":
initialize: function () {
this.collection = new ProjectsCollection();
this.collection.bind("reset", _.bind(this.render, this));
this.collection.fetch(...);
}
If you want to listen for "change" events on the contained models then you can bind a "change" handler to the collection since:
You can bind "change" events to be notified when any model in the collection has been modified,
[...]
Any event that is triggered on a model in a collection will also be triggered on the collection directly, for convenience.
The collection will also generate "add" and "remove" events as the collection itself changes.
Newer versions of Backbone no longer reset collections during fetch:
When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true}, in which case the collection will be (efficiently) reset.
And set:
[...] performs a "smart" update of the collection with the passed list of models. If a model in the list isn't yet in the collection it will be added; if the model is already in the collection its attributes will be merged; and if the collection contains any models that aren't present in the list, they'll be removed. All of the appropriate "add", "remove", and "change" events are fired as this happens
So with newer versions of Backbone you'll want to list for the "add", "remove", and "change" events (which a collection based view should be listening for anyway); you could also use {reset: true} on the initial fetch and listen to "reset" as well. I'd recommend the following approach for collection based views:
Listen to "add" and handle that event with a callback that simply adds one item to the view, don't throw everything away and re-render.
Listen to "remvoe" and handle that event with a callback that only removes the newly removed model.
Listen to "change" and handle that with a callback that replaces (or updates) the appropriate item.
Listen to "reset" and bind that to render. Then pass {reset: true} to the collection's initial fetch call.
That will trap the important events and the collection-view will do the minimal amount of work to handle each one. Of course, this strategy isn't applicable to all situations but I think it is a good starting point.
This changed in 1.0
http://backbonejs.org/#changelog
"If you'd like to continue using "reset", pass {reset: true}."
Ok, so until some one can explain why binding didn't work, I used following workaround:
initialize: function () {
var self = this;
this.collection = new ProjectsCollection();
this.collection.fetch({ success: function () { self.render(); } });

Resources