Backbone model.destroy(): Is explicit removal from collection necessary? - backbone.js

I have a simple question. I am looking at a function with 2 lines of code:
deleteTask: function() {
this.parent.collection.remove(this.model);
this.model.destroy();
}
If I comment out the first line, which is supposed to remove the model from its collection, things seem to work as intended (as in, the model is removed automatically). From Backbone's website, this is the relevant discription for a model's "destroy" function:
Triggers a "destroy" event on the model, which will bubble up through any collections that contain it.
Am I safe to assume that the removal of this.parent.collection.remove(this.model); will not affect the functionality of the code in any way? This is what I think, but I wanted to make sure of it.
Thank you!

If you destroy a model, it is removed from any collections that was containing it. You can see that in the backbone source
//Internal method called every time a model in the set fires an event.
_onModelEvent: function(event, model, collection, options) {
...
if (event === 'destroy') this.remove(model, options);
So yes, I wouldn't think you would need to remove the model from your collection explicitly.
But don't trust me, test for yourself :)
deleteTask: function() {
that = this;
this.model.destroy({
success: function() {
console.log(that.parent.collection);
}
});
}
Check the console for yourself to see whether the model was removed from the collection.

The solution is to override the Backbone model destroy function. I made this on an abstract model with success and callback strategy:
Parameter "data" corresponds to the original parameter "resp".
destroy: function(successCallback, errorCallback)
{
var options = { wait: true };
var model = this;
successCallback = successCallback || function() {};
errorCallback = errorCallback || function() {};
var destroy = function()
{
model.trigger('destroy', model, model.collection, options);
};
options.success = function(data)
{
if ('SUCCESS' === data.responseCode)
{
if (options.wait || model.isNew())
destroy();
successCallback(data);
if (!model.isNew())
model.trigger('sync', model, data, options);
}
else
{
errorCallback(data);
}
};
if (this.isNew())
{
options.success();
return false;
}
var xhr = this.sync('delete', this, options);
if (!options.wait)
destroy();
return xhr;
}

Related

Overriding destroy() to remove photos associated with model doesn't remove model itself

In my Appcelerator Titanium Alloy project, I’m attempting to override model.destroy() to remove photos associated with the model. My code removes the photos just fine, but doesn’t actually delete the model. What am I doing wrong?
_.extend(Model.prototype, {
destroy: function (options) {
// override default destroy method to also remove photos
console.log('destroying the model');
var model = this;
options = options ? _.clone(options) : {};
var photos = JSON.parse(model.get('photos'));
photos.forEach(function (photo) {
console.log("Deleting photo: " + photo);
var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, photo);
f.deleteFile();
});
model.trigger('destroy', model, model.collection, options);
}
});
I see the console log statements, and the photos are removed. But the model remains.
Some things I've tried, without success:
In my controller, calling both of these:
model.destroy();
collection.remove(model);
// also collection.remove([model]);
I've tried adding this to my extended destroy function but it didn't work
...
f.deleteFile();
});
model.collection.remove(model);
// and model.collection.remove([model]);
model.trigger('destroy', model, model.collection, options);
With the following, the model is destroyed, but my code doesn't run and the photos aren't deleted.
_.extend(Model, {
...
Try this inside your Model object and not the prototype.
destroy: function (options) {
// override default destroy method to also remove photos
console.log('destroying the model');
var model = this;
options = options ? _.clone(options) : {};
var photos = JSON.parse(model.get('photos'));
photos.forEach(function (photo) {
console.log("Deleting photo: " + photo);
var f = Ti.Filesystem.getFile(Ti.Filesystem.applicationDataDirectory, photo);
f.deleteFile();
});
Backbone.Model.prototype.destroy.call(this);
}
Just a guess: any chance that the options passed to your extended destroy(...) method include { wait: true }? If so the model won't be removed from the collection until the server responds with a sync event.
The way you've overridden destroy(...), that will never happen.

How to dynamically update a Marionette CollectionView when the underlying model changes

Seems like this should be obvious, but there seem to be so many different examples out there, most of which cause errors for me, making me think they are out of date. The basic situation is that I have a MessageModel linked to a MessageView which extends ItemView, MessageCollection linked to a MessageCollectionView (itemView: MessageView). I have a slightly unusual scenario in that the MessageCollection is populated asynchronously, so when the page first renders, it is empty and a "Loading" icon would be displayed. Maybe I have things structured incorrectly (see here for the history), but right now, I've encapsulated the code that makes the initial request to the server and receives the initial list of messages in the MessageCollection object such that it updates itself. However, I'm not clear, given this, how to trigger displaying the view. Obviously, the model shouldn't tell the view to render, but none of my attempts to instantiate a view and have it listen for modelChange events and call "render" have worked.
I have tried:
No loading element, just display the CollectionView with no elements on load, but then it doesn't refresh after the underlying Collection is refreshed.
Adding modelEvents { 'change': 'render' } to the view --> Uncaught TypeError: Object function () { return parent.apply(this, arguments); } has no method 'on'
I also tried this.bindTo(this.collection..) but "this" did not nave a bindTo method
Finally, I tried, in the view.initialize: _.bindAll(this); this.model.on('change': this.render); --> Uncaught TypeError: Object function () { [native code] } has no method 'on'
Here is the code
Entities.MessageCollection = Backbone.Collection.extend({
defaults: {
questionId: null
},
model: Entities.Message,
initialize: function (models, options) {
options || (options = {});
if (options.title) {
this.title = options.title;
}
if (options.id) {
this.questionId = options.id;
}
},
subscribe: function () {
var self = this; //needed for proper scope
QaApp.Lightstreamer.Do('subscribeUpdate', {
adapterName: 'QaAdapter',
parameterValue: this.questionId,
otherStuff: 'otherstuff',
onUpdate: function (data, options) {
console.log("calling sync");
var obj = JSON.parse(data.jsonString);
self.set(obj.Messages, options);
self.trigger('sync', self, obj.Messages, options);
}
});
},
});
Views.MessageCollectionView = Backbone.Marionette.CollectionView.extend({
itemView: Views.MessageView,
tagName: 'ul',
// modelEvents: {
// 'change': 'render'
// },
onAfterItemAdded: function (itemView) {
this.$el.append(itemView.el);
}
});
var Api = {
subscribe: function (id) {
var question = new QaApp.Entities.Question(null, { id: id });
question.subscribe();
var questionView = new QaApp.Views.QuestionView(question);
QaApp.page.show(questionView);
}
};
I am very grateful for all the help I've received already and thanks in advance for looking.
Try this:
var questionView = new QaApp.Views.QuestionView({
collection: question
});

Collection fetch and model parse

I override function parse() in my model, when name or surname from the database is empty I'm asking Facebook API for that:
var Friend = Backbone.Model.extend({
parse : function(response) {
var self = response,
that = this;
if(!response.first_name) {
FB.api('/'+response.fbid, function(response) {
self.first_name = response.first_name;
self.surname = response.last_name;
});
}
return self;
}
});
My problem is that during fetch-ing in collection this values (first_name and surname) are still empty (though console.log in the model shows it properly). How could I resolve it?
Javascript call to FB.api is async, so basically there is no delay between FB.api and return self. Since your console.log(model) is probably immediately after fetch there is no returned data from FB.api since the request is not over.
What you could do is set try to put some callback when your model is updated and listen to it, and if you change the model trigger update method something like ...
Friend.fetch( { success: function(model, response) {
if ( !model.get('first_name') ) {
FB.api('/'+model.get('fbid'), function(fb_response) {
model.set('first_name', fb_response.first_name);
model.set('last_name', fb_response.last_name);
console.log('model updated with facbook info', model);
});
}
}});
Try running ( in your current code ) console.log('updated'); in your FB.api callback to see the delay I'm talking about.

One View connected to multiple models

I have the following problem…
MyView which is connected to two views: TaskModel and UserModel
TaskModel = {id: 1, taskName: "myTask", creatorName: "myName", creator_id: 2 },
UserModel = {id: 2, avatar: "someAvatar"}
The view should display
{{taskName}}, {{creatorName}}, {{someAvatar}}
As you can see the fetch of TaskModel and UserModel should be synchronized, because the userModel.fetch needs of taskModel.get("creator_id")
Which approach do you recommend me to display/handle the view and the two models?
You could make the view smart enough to not render until it has everything it needs.
Suppose you have a user and a task and you pass them both to the view's constructor:
initialize: function(user, task) {
_.bindAll(this, 'render');
this.user = user;
this.task = task;
this.user.on('change', this.render);
this.task.on('change', this.render);
}
Now you have a view that has references to both the user and the task and is listening for "change" events on both. Then, the render method can ask the models if they have everything they're supposed to have, for example:
render: function() {
if(this.user.has('name')
&& this.task.has('name')) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;​​​​
}
So render will wait until both the this.user and this.task are fully loaded before it fills in the proper HTML; if it is called before its models have been loaded, then it renders nothing and returns an empty placeholder. This approach keeps all of the view's logic nicely hidden away inside the view where it belongs and it easily generalizes.
Demo: http://jsfiddle.net/ambiguous/rreu5jd8/
You could also use Underscore's isEmpty (which is mixed into Backbone models) instead of checking a specific property:
render: function() {
if(!this.user.isEmpty()
&& !this.task.isEmpty()) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;​​​​
}
That assumes that you don't have any defaults of course.
Demo: http://jsfiddle.net/ambiguous/4q07budc/
jQuery's Deferreds work well here. As a crude example:
var succesFunction = function () {
console.log('success');
};
var errorFunction = function () {
console.log('error');
};
$.when(taskModel.fetch(), userModel.fetch()).then(successFunction, errorFunction);
You could also pipe the request through using the crude data (remember that fetch, save, create are really just wrappers around jQuery's $.ajax object.
var taskModelDeferred = taskModel.fetch();
var userModelDeferred = taskModelDeferred.pipe(function( data ) {
return userModel.fetch({ data: { user: data.userId }});
});
note: Backbone returns the collection and model in the success / error functions by default on collections and models so if you need this be sure have a reference handy.
I've run into this very same issue with a complex layout that used two models and multiple views. For that, instead of trying to synchronize the fetches, I simply used the "success" function of one model to invoke the fetch of the other. My views would listen only to the change of the second model. For instance:
var model1 = Backbone.Model.extend({
...
});
var model2 = Backbone.Model.extend({
...
});
var view1 = Backbone.View.extend({
...
});
var view2 = Backbone.View.extend({
...
});
model2.on("change",view1.render, view1);
model2.on("change",view2.render, view2);
Then...
model1.fetch({
success : function() {
model2.fetch();
}
});
The point to this is you don't have to do any sophisticated synchronization. You simply cascade the fetches and respond to the last model's fetch.

How to trigger the fetch method and how to set the url

I'am redesigning my backbone application based on the answer of #20100 to this question The best way to fetch and render a collection for a given object_id.
Please read the comment on the code because I think is more clear, and my question looks better in smaller sizes.
// My View
define([
"js/collections/myCollection",
"js/models/myFeed"
], function (MyCollection, MyModel) {
var MyView = Backbone.View.extend({
tagName: 'ul',
initialize: function () {
this.collection = new MyCollection();
this.collection.on('add', this.onAddOne, this);
this.collection.on('reset', this.onAddAll, this);
// when I make myView = new MyView(_.extend( {el:this.$("#myView")} , this.options));
// myView.render is not called
// in order to trigger the render function I make the following… but probably there is a better way …
var that = this;
this.collection.fetch({
success: function () {
that.render();
}
});
}
});
return MyView;
});
// MyCollection
define([
"js/models/myModel"
], function (MyModel) {
var MyCollection = Backbone.MyCollection.extend({
model: MyModel, // add this
url: function () {
var url = "http://localhost/movies";
return url;
// if I look to the GET request the url is without idAttribute
// how can I attach the idAttribute to this url?
// should bb takes care of this?
}
});
return MyCollection;
});
//MyModel
define([
], function () {
var MyModel = Backbone.MyModel.extend({
idAttribute: 'object_id'
});
return MyModel
});
There's two paths you want to explore
Pre-populate your collection with your model data
In your example you're already doing this, but you're fetching a collection, the collection URL is http://localhost/movies, if you want an individual model take a look at the next point
Fetch each individual model only when you need it
In the assumption that you're trying to get an ID on a collection that is not pre-populated and are loading 1 model at a time, you will have to approach this a bit in a custom way by adding a method to your collection somewhat similarly to this
getOrFetch: function(id, options)
{
var model;
if (this.get(id))
{
model = this.get(id);
}
else
{
model = new this.model({
id: id
});
this.add(model);
model.fetch(options);
}
return model;
}
or add the function as Backbone.Collection.prototype.getOrFetch so you can use it on every Backbone Collection if you need it.

Resources