Backbone.js firing Collection change event multiple times - backbone.js

In one of by Backbone.js views I am updating the attribute "read" of the current model (instance of Message) by using this.model.set( { read: true } );. I verified that this command is only executed once (I know about "ghost events"). As you can see below I configured the Collection to fire an update event in which the whole Collection gets saved into a variable.
Unfortunately the saveToVar function gets called 3 times instead of one! Also, the first time saveToVar is called, this correctly consists of all the collection's models, whilst the 2nd and 3rd time this only has one model, namely the one I did the update on.
I tracked everything down piece by piece but I have no clue why this happens.
window.Message = Backbone.Model.extend({
});
window.MessageCollection = Backbone.Collection.extend({
model: Message,
initialize: function()
{
this.on("change", this.saveToVar);
},
saveToVar: function(e)
{
App.Data.Messages = this.toJSON();
return;
}
});

In your jsfiddle, you're doing this:
App.Collections.message = new MessageCollection([ ... ]);
var elements = App.Collections.message.where({ id: 4 });
var item = new MessageCollection(elements);
Your where call will return models that are in the message collection, not copies of those models but exactly the same model objects that are in message. Now you have two references to your id: 4 model:
The original one buried inside App.Collections.message.
The one in elements[0].
Both of those references are pointing at the same object. Then you add elements to another MessageCollection. Now you have something like this:
App.Collections.message.models[3] item.models[0]
| |
+--> [{id: 4}] <--+
Both of those collections will be notified about change events on the id: 4 model since collections listen to all events on their members:
Any event that is triggered on a model in a collection will also be triggered on the collection directly, for convenience.
And your collection listens for "change" events in itself:
initialize: function()
{
this.on("change", this.saveToVar);
}
So when you do this:
this.model.set({ read: true });
in your view, both collections will be notified since that model happens to be in both collections.
If we alter your event handler to look like this:
saveToVar: function() {
console.log(_(this.models).pluck('cid'));
}
then you'll see that the same cid (a unique identifier that Backbone generates) appears in both collections. You can also attach a random number to each collection and see what you get in saveToVar: http://jsfiddle.net/ambiguous/mJvJJ/1/
You probably shouldn't have one model in two collections. You probably shouldn't have two copies of the same model kicking around either so cloning elements[0] before creating item might not be a good idea either. You might need to reconsider your architecture.

Related

Backbone model which I see in success callback and error callback is different. #Backbone save

I have a backbone model which has Backbone Collections in it. When I save the model and if it is success then my model object is properly structured as it was. But when error occurs (say validation error), in error callback the model object is modified (Collections inside model object are converted into Array). As a result all my functions defined for that Collections are now "undefined" and gives me error.
save : function() {
this.model.save(_.extend(originalModel.toJSON() || {}, this.model
.toJSON()), {
success : this.onSaveSuccess,
error: this.onSaveError,
include : []
});
},
onSaveSuccess : function(model) {
//Here the model is properly structured
},
onSaveError : function(model, response) {
// Here the model is modified, all collections are now array
//I have to explicitly call my parse method to re structure it.
model = model.parse(model.attributes);
}
I would like to know why is this happening. Am I doing something wrong here ?
For the sake of this example, let's assume the attribute of the model that holds the collection is called "people". It isn't clearly documented, but model.save(attributes) actually behaves like:
model.set(attributes);
model.save();
Here's the relevant annotated source of save(...). What your code is doing is first setting the "people" attribute to the array of people, then attempting to save it. When the save fails, your model has the array, not the collection, as the value of "people".
I suspect your end point is returning the full representation of the model on success, and your model is correctly parsing that representation & re-building the Collection at that point. But your error handler won't do that automatically.
As an aside, in my experience Models that contain Collections are hard to manage & reason about. I've had better luck having a Model that contains an array of data, and then having a method on that Model to build a Collection on the fly. Something like:
var MyModel = Backbone.Model.extend({
// ...
getPeople: function() {
// See if we've previously built this collection
if (!this._peopleCollection) {
var people = this.get('people');
this._peopleCollection = new Backbone.Collection(people);
}
return this._peopleCollection;
}
});
This removes the Collection concept from the server communication (where it's pretty unnecessary), while also providing a smarter data layer of your application (smart Models are a good thing).
The solution for this is passing wait:true in options. This will not modify until and unless server returns a valid response.
save : function() {
this.model.save(_.extend(originalModel.toJSON() || {}, this.model
.toJSON()), {
success : this.onSaveSuccess,
error: this.onSaveError,
**wait:true**
include : []
});
},

Backbone Collection get property

Got a server returning a JSON object like so:
{
'key1':'value'
'key2':{
'key2_0':'value'
}
}
And a collection:
var Collection = Backbone.Collection.extend({
url:api.url//which returns the object above
});
var collection = new Collection();
collection.fetch({
success:function(data){
//do something
}
});
Now i need to use certain properties of the collection throughout my application, but say i need key1, i always have to do collection.at(0).get('key1');//returns 'value', because the data returned is stored within the collection, in a new Array at key 0.
Question:
How to directly... collection.get('key1')//now returns undefined... because it is.
I know i could expose an object to the global scope in the collection success function some_other_var = data.toJSON()[0] and access the some_other_var properties directly, but that's not what i'm looking for;
In order to use the get() function from a Backbone.Collection you need to know the model id or cid wanted.
For instance, lets say your data coming from the server is like follow:
[{
id: '123',
name: 'Alex'
}, {
id: '456',
name: 'Jhon'
}]
In that case you can do this:
this.collection.get('123').get('name') // Return "Alex"
Keep in mind that collection is just a set of model, so behind the scenes by doing collection.get() you are getting a model
Tip: If you don't have any kind of id in your server data, there is always the option of using underscore methods:
find
filter
some
contains
etc
It seems like you're trying to ascribe attributes to a collection, but a collection is merely a set of models. Having additional data that is constant throughout the collection suggests that it should be wrapped inside another Model, which is demonstrated here: Persisting & loading metadata in a backbone.js collection

Backbone model structure gets changed when returning them from web worker

I am trying to reset a backbone collection with an array of models. It gets reset but the model structure is changed (nested one level).
Here is a detailed explanation:
Model
var SeatModel = Backbone.Model.extend({
defaults:{
},
initialize:function () {
console.log('Model initialized');
}
});
Collection
var myCollection = Backbone.Collection.extend({
url:"",
parse:function (data) {
},
initialize:function () {
console.log('Collection initialized');
}
});
Now, I am executing some logic in a web worker, which generates an array of models. The size of the array varies depending on the url I hit.
When the array is ready, I reset the data in the collection using something like:
(Before this, I have instantiated the collection and set it in an service object)
worker.onmessage = function(e) {
newDataForCollection = e.data;
//update the collection
service.get("myCollection").reset(newDataForCollection);
};
After getting reset, the structure of the collection gets changed to something like:
models: Array[3154]
[0...99]
0:g.Model
attributes:
attributes:
price: "12"
Whereas it should be like:
models: Array[3154]
[0...99]
0:g.Model
attributes:
price: "12"
Also the number of models in the array gets reduced. (Should have been around 6100 in this case).
I am unable to figure out, what causes the internal structure to get nested by one level on invoking reset on the collection.
Updated Post
Figured it out. We cannot send objects with functions in post message, so the models in the array just have the attributes and no functions. Related Passing objects to a web worker
Figured it out. We cannot send objects with functions in post message, so the models in the array just have the attributes and no functions. This was related to issue Passing objects to a web worker

Save Backbone.js Model and update entire Collection

I would like to be able to call save() on a backbone model and have the backend return the entire collection of this model instead of only the changed attributes of the model. I would then like backbone to update the entire returned collection. The use case for this is the following:
A user has multiple addresses and can choose a shipping address from this collection. If she chooses a different shipping address from the collection the previous shipping address should be updated to the state of 'just another plain address'. For this the entire collection has to be updated instead of only the changed model.
Is this somehow possible in backbone.js?
Thanks a lot in advance!
models that are bound to collections, contain their collection parent as a property. Also, since your returning a list of models, we can assume that it is always in a list.
mymodel = Backbone.Model.extend({
parse: function (data) {
if(this.collection && typeof(data) === 'array') {
this.collection.reset(data);
}
return data;
}
});
I do not think that overriding sync or breaking the expectations of what save returns is necessary here.
It would be simpler I guess to override save on the model, something on the lines of:
save: function (key, value, options) {
var p = Model.prototype.save.call(this, key, value, options),
self=this;
if (this.collection) {
p.done(function () { self.collection.fetch(); });
}
return p;
}
which will save using the normal save obtaining its promise, and then if saving was successful and the model is part of a collection, it will fetch the collection from the server.
Another way would be to bind to the model's change event, check if it belongs to a collection and fetch, but that would also happen on set.
Yep, it's possible. You'll need to override the sync function on the model
MyModel = Backbone.Model.extend({
sync: function(method, model) {
if (method === 'create') {
//perform save logic and update the model's collection
} else if (method === 'update') {
//perform save logic and update the model's collection
} else if (method === 'read') {
...
} else if (method === 'destroy') {
...
}
}
});
Take a look at the Backbone.sync function for more information.
What your use case actually calls for is the updating of two models, not the updating of an entire collection. (Other than fetching, collections don't interact with the server in Backbone.) Assuming you have addresses A, B, and C, with A as the current shipping address and C as the new shipping address, your code can simply:
Update C to be the new shipping address.
Update A to be 'just another address.'
If your problem is that you don't know which address is the current address (i.e., address A), then you can just search inside your collection for it (i.e., "give me the current shipping address"), update C, and then update the returned address (address A).
If you absolutely need to update an entire collection (you don't), cycle through collection.models and update each one individually. There simply is no concept in Backbone of a collection being acted upon RESTfully.
Edit: I may have misread your question. If you meant update the collection on the client (i.e., you didn't intend to update the collection on the server), then the code is still similar, you just update both the old model and the new one.

merge backbone collection with server response

TL;DR: If I'm polling the entire collection of models from the server, how can I merge the changed attributes into each model and add/remove added/removed models from the collection?
In my backbone app, I am polling for the entire collection of models. I have a Backbone.Collection that I am basically calling reset on each time I get the array of models, so:
myCollection.reset(server_response);
The only problem with this is that it gets rid of the old models, kind of eliminating the benefit of events on a model. This is reset's purpose of course, but what I want to do is only modify the changed attributes of the models, and remove models not in the response, and add models that were in the response but not the collection.
Essentially I want a sort of merge of the data.
For models that are in the response and in the collection already, I believe I can just do model.set(attributes) and it takes care of seting only the ones that actually changed, triggering the change events in the process. This is great.
But how do I handle the cases where the models were in the response but not in the collection already, and vice versa, not in the response but in the collection?
My Proposed Solution
I don't know if backbone already has a way of doing this, and I may be overcomplicating which is why I'm asking, but I was thinking then of creating a method on my collection which gets passed the server_response.
It would get all of the id attributes of the server_response, and all of the id attributes of the models already in the collection.
The difference of the id's in response - collection would = added models, and vice versa would be removed models. Add and remove those models respectively from the collection.
The intersection of both sets of id's would be the modified models, so iterate through these id's and simply do a collection.get(id).set(attributes).
In pseudocoffeescript:
merge: (server_response) =>
response_ids = _.pluck(server_response, 'id')
collection_ids = #pluck('id')
added = _.difference(response_ids, collection_ids)
for add in added
#add(_.find(server_response, (model) ->
return model.id == add
))
removed = _.difference(collection_ids, response_ids)
for remove in removed
#remove(#get(remove))
changed = _.intersection(response_ids, collection_ids)
for change in changed
#get(change).set(_.find(server_response, (model) ->
return model.id == change
))
This technique is useful sometimes. We extend Collection with the following method. This should do what you're looking for. It's not in coffee, but you could easily port it. Enjoy!
// Take an array of raw objects
// If the ID matches a model in the collection, set that model
// If the ID is not found in the collection, add it
// If a model in the collection is no longer available, remove it
freshen: function (objects) {
var model;
// Mark all for removal
this.each(function (m) {
m._remove = true;
});
// Apply each object
_(objects).each(function (attrs) {
model = this.get(attrs.id);
if (model) {
model.set(attrs); // existing model
delete model._remove
} else {
this.add(attrs); // new model
}
}, this);
// Now check for any that are still marked for removal
var toRemove = this.filter(function (m) {
return m._remove;
})
_(toRemove).each(function (m) {
this.remove(m);
}, this);
this.trigger('freshen', this);
}
This just went into Backbone master 3 days ago:
https://github.com/documentcloud/backbone/pull/1220

Resources