How can I persist custom attributes over a collection fetch - backbone.js

I have a an "Asset" backbone model that has a custom attribute called "selected". Its custom in the sense that it is not part of the object on the server side. I use to represent which of the list of assets the user has currently selected.
var Asset = Backbone.Model.extend({
defaults: {
selected: false
},
idAttribute: "AssetId"
});
This model is part of a backbone collection that I fetch periodically to get any changes from the server.
The problem I have is that every time I fetch the collection, the collection is doing a reset (I can tell by the listening for the reset event) and hence the value of the selected attribute is wiped out by the data coming in from the ajax request.
The backbone.js documentation seems to suggest that there is a intelligent merge that will solve this problem. I believe I'm doing this in my fetch methods
allAssets.fetch({ update: true ,cache: false});
And I have also set the "idAttribute" field in the model so that the ids of the object coming in can be compared with the objects in the collection.
The way I have solved this is by writing my own Parse method in my collection object
parse: function (response) {
// ensure that the value of the "selected" for any of the models
// is persisted into the model in the new collection
this.each(function(ass) {
if (ass.get("selected")) {
var newSelectedAsset = _.find(response, function(num) { return num.AssetId == ass.get("AssetId"); });
newSelectedAsset.selected = true;
}
});
return response;
}
Is there a better way to do this?

Collection.update (introduced in Backbone 0.9.9) does indeed try to merge existing models, but does so by merging all set attributes in the new model into the old model. If you check Backbone source code, you'll see
if (existing || this._byCid[model.cid]) {
if (options && options.merge && existing) {
existing.set(model.attributes, options);
needsSort = sort;
}
models.splice(i, 1);
continue;
}
All attributes, including defaults, are set, that's why your selected attribute is reset to false. Removing the default value for selected will work as intended: compare http://jsfiddle.net/nikoshr/s5ZXN/ to http://jsfiddle.net/nikoshr/s5ZXN/3/
That said, I wouldn't rely on a model property to store my app state, I would rather move it to a controller somewhere else.

Related

Fetch a backbone collection with only the models with specified value

I have a dictionary of type {name: value}
A = {
name: x,
name: y,
name: z
}
I want to fetch a collection (consisting of models having one of their attribute as 'name'), but to be optimal I want to fetch such that the value of the attribute 'name' exists in my dictionary.
Is there a way to do specific filtering like that?
If you're doing the filtering client-side, overriding the filter method is really NOT the way to go.
Now you no longer have it available, should you need it later. Also, modifying the collection itself from within the filter method is an undesirable sideeffect.
Instead you should be using the parse method, which will automatically be called when fetching the collection.
Now as I understand it, you want to limit the fetched set to models with names matching the keys in your dictionary.
If so, I would do the following:
parse: function(response, options) {
// Do we want to filter the response?
if (options.filterNames) {
// Filter
response = _.filter(response, function(obj) {
// Check if this model name is one of the allowed names
return _.contains(options.filterNames, obj.name);
});
}
// Backbone will use the return value to create the collection
return response;
}
And then call fetch using
someCollection.fetch({filterNames: _.keys(someDictionary)});
If you're certain, you will always be filtering the collection on fetch, you can omit passing the option and just use the dictionary within parse.
Alternatively you could create a fetchFiltered() method on the collection, which would then invoke the line above.
After investigations and trials, here are the two ways this can be resolved:
1. Client side filtering after fetching the collection from the server. This is a less optimal method, especially when the collection is huge. In situations when you really want 5 models out of a 1000 model collection, it can be an overkill. But if the server side has no logic of accepting and using the filtering client side filtering should look something like:
Overload the collection filter code something like:
var filter = {
filter: function() {
var results = _.filter(this.models, function(model) {
// Perform the check on this model, like compare it to your local dict
if (checkPassed) {
return true;
}
return false;
});
results = _.map(results, function(model) {
return model.toJSON();
});
// Reset the existing collection to filtered models
this.reset(results) ;
};
var ExtendedCollection = OriginalCollection.extend(filter);
Pass a filter option in the fetch ajax call to the server, and the server should understand the filter and return the collection based off that.

Marionette Regions and routing

I'm using a LayoutView to display a collection in table form. When a user clicks on a tr I swap the CompositeView for an ItemView that shows the details using the same region. It all works except the functionality of the back button is broken. Is there a way to trap the back event and switch views?
Or should I use two Views and pass the model id and then refetch the model? The problem with that though is the extra request and I lose the filter and sort values of the table unless I use local storage.
Including more code would be better, but in any case I will try to give some guidance for your problem.
To avoid fetching the data twice, you can keep a common object in a "parent" component, for example in the Router.
var theObject;
var router = Marionette.AppRouter.extend({
routes: {
"routeA/:id": "goToRouteA",
"routeB/:id": "goToRouteB"
},
goToRouteA: function(id) {
MyRegion.show(new myLayout({
model: this._getCommonObject(id)
}));
},
goToRouteB: function(id) {
MyRegion.show(new myLayout({
model: this._getCommonObject(id)
}));
},
/*Return the common object for the views*/
_getCommonObject: function(id) {
theObject = (theObject && theObject.get('id') == id) ? theObject : MyApp.request('getTheObject');
return theObject;
}
});
In this way, you can keep the reference to the same object without loosing information.
You just have to make sure to delete the object when it is not needed to avoid keeping old information, for example on the Region close event.

Backbone - use different views or templates for same collection

The base model of my application has a status attribute.
Let's assume, for simplicity, that status might be either pending or deleted.
I have an upper menu with these two status values, when you click one of them you see all objects with this status (I use router to trigger a filter).
My problem is that I need to draw a different template for each status.
deleted object has delete forever and recover buttons
pending object has delete, edit and some other buttons (also some textarea...)
I wonder what would be the best solution for this problem.
I thought of creating a different view for each status, but then I don't know how to deal with it in the collection level.
I also thought of creating different templates and deal with it in the model-view level, but again - I have no idea whether it is possible and if yes - how.
Finally, I can solve it with same template and view, hiding what is not necessary inside the view, but then the code becomes quite ugly in my point of view.
Ideas?? Thanks!
If you want to create a different view for each status, you do it this way :
Router {
clickDeletedMenu : {
var collection = new MyCollection();
var deletedView = new DeletedView({ model : collection });
collection.fetch({ status : 'deleted' }); // filter deleted objects
},
clickPendingMenu : {
var collection = new MyCollection();
var pendingView = new PendingView({ model : collection });
collection.fetch({ status : 'pending' }); // filter deleted objects
},
}
If you want to create differents templates, you do it this way :
View {
render : {
if (this.model.status == 'deleted') {
// render deleted template
} else {
// render pending template
}
}
}
Finally, in my point of view, you can use the same template and view, and hiding what is not necessary inside the template not the view.
nb : the code is used just to illustrate the idea, it's not going to execute :)

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