How to sort a collection in Backbone - backbone.js

I want to sort my colection before I pass it to the template. I use in the render function of my view
CollectionForTelplate : this.Collection
I do the fetch as
var self = this;
//fetch done here
if (Collection.length > 0) {
_.each(Collection, function(Model) {
JSON.stringify(Model);
}, this);
};
self.Collection = Collection;
self.render;
Is there any other way by means of which I can pass the collection to the template?
How do you sort a collection based on a string field of the model, say Model.name ? I tried writing a comparator in the collection and a sort function the view but unfortunately; it doesnot work for me

Implement your Collection's compatarator function, defined in the docs as
If you define a comparator, it will be used to maintain the collection
in sorted order.
That way your collection will be automatically kept in a sorted order after adds, removals etc. You can implement it as sort
comparator: function(model1, model2) {
if (model1 comes before model 2) {
return -1;
} else if (model1 is equal to model 2) {
return 0;
} else { // model1 comes after model 2
return 1;
}
}
or sortBy
comparator: function(model) {
// Return some numeral or string attribute and it will be ordered by it
// == smaller numbers come first / strings are sorted into alphabet order
return model.get('someAttribute');
}
Hope this helps!

Is there any other way by means of which I can pass the collection to
the template?
Yes, collection's have a toJSON() method so you can simply do something like
render: function() {
this._template({list: this.toJSON()}); //assuming your template is already compiled
return this;
}
How do you sort a collection based on a string field of the model, say
Model.name ? I tried writing a comparator in the collection and a sort
function the view but unfortunately; it does not work for me
You can simply define the comparator function on the collection, and it should keep itself sorted, here's the example given in the docs
chapters.comparator = function(chapter) {
return chapter.get("page");
};

Related

Multiple collections tied to one base collection with filters and eventing

I have a complex model served from my back end, which has a bunch of regular attributes, some nested models, and a couple of collections.
My page has two tables, one for invalid items, and one for valid items. The items in question are from one of the nested collections. Let's call it baseModel.documentCollection, implementing DocumentsCollection.
I don't want any filtration code in my Marionette.CompositeViews, so what I've done is the following (note, duplicated for the 'valid' case):
var invalidDocsCollection = new DocumentsCollection(
baseModel.documentCollection.filter(function(item) {
return !item.isValidItem();
})
);
var invalidTableView = new BookIn.PendingBookInRequestItemsCollectionView({
collection: app.collections.invalidDocsCollection
});
layout.invalidDocsRegion.show(invalidTableView);
This is fine for actually populating two tables independently, from one base collection. But I'm not getting the whole event pipeline down to the base collection, obviously. This means when a document's validity is changed, there's no neat way of it shifting to the other collection, therefore the other view.
What I'm after is a nice way of having a base collection that I can have filter collections sit on top of. Any suggestions?
I fleshed out my previous attempt and have come up with an extension to Backbone.Collection that does what I need.
collections.FilteredCollection = Backbone.Collection.extend({
initialize: function(items, options) {
if (_.isUndefined(options.baseCollection))
throw "No base collection to watch";
if (!_.isFunction(options.filterFunc)) {
throw "No filter to apply";
}
_.extend(this, options);
this.listenTo(this.baseCollection, 'all', this.reraise);
},
reraise: function (event) {
this.reset(this.baseCollection.filter(this.filterFunc), { silent: true });
var args = [].slice.call(arguments, 1);
this.trigger(event, args);
}
});
The one small issue I have with this is that I have to manually apply filterFunc to the baseCollection, then pass that in as the items parameter when instantiating a FilteredCollection, but that's something I can live with.
The below code is what I'm using to instantiate. Note that there's another almost-exact copy which is for the collection of ONLY VALID items, but any filters can be applied.
var allDocs = theModel.get('Documents');
var invalidOptions = {
baseCollection: allDocs,
filterFunc: function(item) {
return !item.isValidItem();
}
};
var invalidDocs = allDocs.filter(invalidOptions.filterFunc);
var invalidDocsCollection = new collections.FilteredCollection(
invalidDocs, invalidOptions
);

what is the correct way to get element from collection

In my backbone view, I am getting models from my collection..
initialize:function(){
this.collection = new collection(student);
this.render();
},
from this collection I am filtering the high value models using filter method:(on click I am triggering)
getHighSocre:function(){
return _.filter(this.collection.models, function(item){
return parseInt(item.get('scored')) > 60
})
},
showHighScore:function(){
var hView = new studentView({model:this.getHighSocre()}); // sending to single view
hView.showMe(); // I am calling this method to add a class name to each 'li' element..
}
here is my single view:
var studentView = Backbone.View.extend({
tagName:'li',
events:{
'click':"called"
},
template:_.template($("#studentTemplate").html()),
render:function(){
this.$el.append(this.template(this.model.toJSON()));
return this;
},
called:function(){
if(this.model.get('scored') > 60){
this.$el.css({
background:"#EFEFEF"
})
}else{
this.$el.css({
background:"#FCBABA"
})
}
},
showMe:function(){ // I am passing here to add a class name
console.log(this) // make the array... here
this.$el.css({ // but this is not getting the dom element...
border:'1px solid red'
})
}
});
How to I add the class name to each of this li elements? What is wrong here, anyone can help me to sort or can give me a correct way to filter a collection and apply the class name to it's element?
Here is the jsfiddle.
First off, with Backbone and Underscore, you usually don't want to call Underscore methods on Collections, eg:
_.filter(this.collection.models, function(item){
you instead want to call the Backbone Collection equivalent method (http://documentcloud.github.io/backbone/#Collection-Underscore-Methods):
this.collection.filter(function(item){
Second you misspelled "score" as "socre"; not trying to be a jerk, just pointing it out because such misspellings can easily cause bugs.
Third, views expect a model for their model parameter, but your getHighSocre method returns the result of a filter, ie. an array of models, so this line:
new studentView({model:this.getHighSocre()});
isn't going to work. If you just want the first model with a score above 60, try using find instead of filter, and if you really do want your view to have every model with a score above 60 then you probably want to convert those models in to a new collection and pass that as the view's collection (rather than as its model).
P.S.
This isn't really part of the answer, but just a note; if you weren't familiar with Javascript's ternary operator, you might want to check it out as it lets you reduce all of this:
if(this.model.get('scored') > 60){
this.$el.css({
background:"#EFEFEF"
})
}else{
this.$el.css({
background:"#FCBABA"
})
}
to just:
var isAbove60 = this.model.get('scored') > 60;
this.$el.css('backgroundColor', isAbove60 ? "#EFEFEF" : "#FCBABA");

Underscore - Filter against an array of values

I have a collection self.models. I also have an array of an object which contains the fields and filters I wish to apply to my collection called filterArr. An example of this would be:
[{field: "Account", filter: "123"}, {field: "Owner", filter: "Bob"}]
The question is, I'm not sure quite how I'd iterate through each of my models to return only those models to which this filterArr applies too, I know it has to be something like this, but this is hard-coded:
self.models = _.filter(self.models, function (model) {
model = model.toJSON();
return model.Account === "123" && model.Owner === "Bob";
});
First of all, underscore's filter returns an Array, so what you're doing effectively here is substituting your collection with a filtered array. Something like this would be more appropriate:
this.filtered = _.filter(this.models, ...);
Backbone Collection implements most of underscore's useful functions. So the solution above is far from optimal (in fact it doesn't work at all the way you want it to), instead do something like this:
this.filtered = this.models.filter(function() {...});
The best way to get and set model fields by name are by far the get and set functions of Backbone Model, so why not use them? Model.toJSON() works, but you're just copying the attributes-hash unnecessarily around.
this.filterObj = { // Why not make it an object instead of array of objects
"Account": "123",
"Owner": "Bob"
};
this.filtered = this.models.filter(function(model) {
// use the for in construct to loop the object
for (filter in filterObj) {
// if the model doesn't pass a filter check, then return false
if (model.get(filter) !== filterObj[filter]) return false;
}
// the model passed all checks, return true
return true;
});
Hope this helps!
Basically you need to iterate over model's attributes and compare their keys and values to filter's attributes.
self.models = _.filter(self.models, function (model) {
var fits = true; // does this model "fit" the filter?
model = model.toJSON();
_.each(model, function(modelVal, modelKey) {
_.each(filterArr, function(filter) {
if (modelKey === filter.field && modelVal !== filter.filter) {
fits = false
}
}
})
return fits
})
However, with a bit of underscore magic there's a trickier way. I'm not sure if it's better in terms of performance, but it surely looks better to my eye.
// change a bit the way filter is described:
var filter = {Account: '123', Owner: 'Bob'},
// save an array of filter keys (Account, Owner)
filterKeys = _.keys(filter),
// and an array of its values (123, Bob)
filterVals = _.values(filter)
self.models = _.filter(self.models, function (model) {
// pick a subset of model which has the same keys as filter
var filteredSubset = _.pick(model.attributes, filterKeys),
// pick values of this subset
subsetValues = _.values(filteredSubset)
// this values have to be equal to filter's values
// (use .join() to turn array to string before comparison due to references)
return filteredVals.join() === subsetValues.join()
})
Notice that in the latter case all models have to have all the keys declared in filter.
If I were you and I were looking for a most robust way, I would rewrite the first example, but would have changed _.each to standard for loops and return false as soon as first 'non-fit' value is met.

Proper way to sort a backbone.js collection on the fly

I can successfully do this:
App.SomeCollection = Backbone.Collection.extend({
comparator: function( collection ){
return( collection.get( 'lastName' ) );
}
});
Which is nice if I want to have a collection that is only sorted by 'lastName'. But I need to have this sorting done dynamically. Sometimes, I'll need to sort by, say, 'firstName' instead.
My utter failures include:
I tried passing an extra variable specifying the variable to sort() on. That did not work. I also tried sortBy(), which did not work either. I tried passing my own function to sort(), but this did not work either. Passing a user-defined function to sortBy() only to have the result not have an each method, defeating the point of having a newly sorted backbone collection.
Can someone provide a practical example of sorting by a variable that is not hard coded into the comparator function? Or any hack you have that works? If not, a working sortBy() call?
Interesting question. I would try a variant on the strategy pattern here. You could create a hash of sorting functions, then set comparator based on the selected member of the hash:
App.SomeCollection = Backbone.Collection.extend({
comparator: strategies[selectedStrategy],
strategies: {
firstName: function () { /* first name sorting implementation here */ },
lastName: function () { /* last name sorting implementation here */ },
},
selectedStrategy: "firstName"
});
Then you could change your sorting strategy on the fly by updating the value of the selectedStrategy property.
EDIT: I realized after I went to bed :) that this wouldn't quite work as I wrote it above, because we're passing an object literal to Collection.extend. The comparator property will be evaluated once, when the object is created, so it won't change on the fly unless forced to do so. There is probably a cleaner way to do this, but this demonstrates switching the comparator functions on the fly:
var SomeCollection = Backbone.Collection.extend({
comparator: function (property) {
return selectedStrategy.apply(myModel.get(property));
},
strategies: {
firstName: function (person) { return person.get("firstName"); },
lastName: function (person) { return person.get("lastName"); },
},
changeSort: function (sortProperty) {
this.comparator = this.strategies[sortProperty];
},
initialize: function () {
this.changeSort("lastName");
console.log(this.comparator);
this.changeSort("firstName");
console.log(this.comparator);
}
});
var myCollection = new SomeCollection;
Here's a jsFiddle that demonstrates this.
The root of all of your problems, I think, is that properties on JavaScript object literals are evaluated immediately when the object is created, so you have to overwrite the property if you want to change it. If you try to write some kind of switching into the property itself it'll get set to an initial value and stay there.
Here's a good blog post that discusses this in a slightly different context.
Change to comparator function by assigning a new function to it and call sort.
// Following example above do in the view:
// Assign new comparator
this.collection.comparator = function( model ) {
return model.get( 'lastname' );
}
// Resort collection
this.collection.sort();
// Sort differently
this.collection.comparator = function( model ) {
return model.get( 'age' );
}
this.collection.sort();
So, this was my solution that actually worked.
App.Collection = Backbone.Collection.extend({
model:App.Model,
initialize: function(){
this.sortVar = 'firstName';
},
comparator: function( collection ){
var that = this;
return( collection.get( that.sortVar ) );
}
});
Then in the view, I have to M-I-C-K-E-Y M-O-U-S-E it like this:
this.collections.sortVar = 'lastVar'
this.collections.sort( this.comparator ).each( function(){
// All the stuff I want to do with the sorted collection...
});
Since Josh Earl was the only one to even attempt a solution and he did lead me in the right direction, I accept his answer. Thanks Josh :)
This is an old question but I recently had a similar need (sort a collection based on criteria to be supplied by a user click event) and thought I'd share my solution for others tackling this issue. Requires no hardcoded model.get('attribute').
I basically used Dave Newton's approach to extending native JavaScript arrays, and tailored it to Backbone:
MyCollection = Backbone.Collection.extend({
// Custom sorting function.
sortCollection : function(criteria) {
// Set your comparator function, pass the criteria.
this.comparator = this.criteriaComparator(criteria);
this.sort();
},
criteriaComparator : function(criteria, overloadParam) {
return function(a, b) {
var aSortVal = a.get(criteria);
var bSortVal = b.get(criteria);
// Whatever your sorting criteria.
if (aSortVal < bSortVal) {
return -1;
}
if (aSortVal > bSortVal) {
return 1;
}
else {
return 0;
}
};
}
});
Note the "overloadParam". Per the documentation, Backbone uses Underscore's "sortBy" if your comparator function has a single param, and a native JS-style sort if it has two params. We need the latter, hence the "overloadParam".
Looking at the source code, it seems there's a simple way to do it, setting comparator to string instead of function. This works, given Backbone.Collection mycollection:
mycollection.comparator = key;
mycollection.sort();
This is what I ended up doing for the app I'm currently working on. In my collection I have:
comparator: function(model) {
var methodName = applicationStateModel.get("comparatorMethod"),
method = this[methodName];
if (typeof(method === "function")) {
return method.call(null, model);
}
}
Now I can add few different methods to my collection: fooSort(), barSort(), and bazSort().
I want fooSort to be the default so I set that in my state model like so:
var ApplicationState = Backbone.Model.extend({
defaults: {
comparatorMethod: "fooSort"
}
});
Now all I have to do is write a function in my view that updates the value of "comparatorMethod" depending upon what the user clicks. I set the collection to listen to those changes and do sort(), and I set the view to listen for sort events and do render().
BAZINGA!!!!

Returning a collection list using a filter from an array of id in backbone.js

So I already got this working using a loop but is wondering is there is a cleaner way to do this.
Basically i got a model that got an array of ids from another model, currently I loop every id and add manually model to a new collection using this filter on the model collection.
getOneById : function(id){
return this.filter(function(data) {
return data.get("id") == id;
});
},
Is there a way to directly return a list doing something like
getAllById : function(arrayIds){
return _(this.filter(function(data) {
??????? return data.get("id") == eachID;
}));
},
Thanks!
you could reduce the looping by checking to see if the id of your object is at an index > -1 in the array:
function(arrayIds){
var models = _.select(collection, function(model){
return (_.indexOf(arrayIds, model.id) > -1);
});
return models;
}
this requires the inclusion of underscore.js in your code, but since you're already using backbone, you already have that.
My solution:
Having collection (Backbone.Collection) and arrayIds
var collection2 = new Backbone.Collection();
collection2.add(collection.models.filter(function(model){
return arrayIds.indexOf(model.id) !== -1;
}));
Four lines :D
console.assert(collection2.length === arrayIds.length) //OK

Resources