Reverse sort order with Backbone.js
With Backbone.js I've got a collection set up with a comparator function. It's nicely sorting the models, but I'd like to revers开发者_如何学编程e the order.
How can I sort the models in descending order rather than ascending?
Well, you can return negative values from comparator. If we take, for example, the example from Backbone's site and want to reverse the order, it will look like this:
var Chapter = Backbone.Model;
var chapters = new Backbone.Collection;
chapters.comparator = function(chapter) {
return -chapter.get("page"); // Note the minus!
};
chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));
alert(chapters.pluck('title'));
Personally, I'm not that happy with any of the solutions given here:
The multiplying by -1 solution fails to work when the sort type is non-numeric. Although there are ways around this (by calling
Date.getTime()
for example) these cases are specific. I would like a general way to reverse the direction of any sort, without having to worry about the specific type of the field being sorted.For the string solution, constructing a string one character at a time seems like a performance bottleneck, especially when using large collections (or indeed, large sort field strings)
Here's a solution that works nicely for String, Date and Numeric fields:
Firstly, declare a comparator that will reverse the result of a result of a sortBy function, like this:
function reverseSortBy(sortByFunction) {
return function(left, right) {
var l = sortByFunction(left);
var r = sortByFunction(right);
if (l === void 0) return -1;
if (r === void 0) return 1;
return l < r ? 1 : l > r ? -1 : 0;
};
}
Now, if you want to reverse the direction of the sort, all we need to do is:
var Chapter = Backbone.Model;
var chapters = new Backbone.Collection;
// Your normal sortBy function
chapters.comparator = function(chapter) {
return chapter.get("title");
};
// If you want to reverse the direction of the sort, apply
// the reverseSortBy function.
// Assuming the reverse flag is kept in a boolean var called reverseDirection
if(reverseDirection) {
chapters.comparator = reverseSortBy(chapters.comparator);
}
chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));
alert(chapters.pluck('title'));
The reason this works is that Backbone.Collection.sort
behaves differently if the sort function has two arguments. In this case, it behaves in the same manner as the comparator passed into Array.sort
. This solution works by adapting the single argument sortBy function into a two argument comparator and reversing the result.
Backbone.js's collection comparator relies on the Underscore.js method _.sortBy. The way sortBy is implemented ends up "wrapping" up javascript .sort() in a way that makes sorting strings in reverse difficult. Simple negation of the string ends up returning NaN and breaks the sort.
If you need to perform a reverse sort with Strings, such as reverse alphabetical sort, here's a really hackish way of doing it:
comparator: function (Model) {
var str = Model.get("name");
str = str.toLowerCase();
str = str.split("");
str = _.map(str, function(letter) {
return String.fromCharCode(-(letter.charCodeAt(0)));
});
return str;
}
It's by no means pretty, but it is a "string negator". If you don't have any qualms with modifying native object types in javascript, you could make you code clearer by extracting the string negator and adding it as a method on String.Prototype. However you probably only want to do this if you know what you are doing, because modifying native object types in javascript can have unforeseen consequences.
My solution was to reverse results after sort.
To prevent double rendering first sort with silent, then trigger 'reset'.
collections.sort({silent:true})
collections.models = collections.models.reverse();
collections.trigger('reset', collections, {});
Modify your comparator function to return some reversely proporitional value instead of returning the data that you are currently. Some code from : http://documentcloud.github.com/backbone/#Collection-comparator
Example:
var Chapter = Backbone.Model;
var chapters = new Backbone.Collection;
/* Method 1: This sorts by page number */
chapters.comparator = function(chapter) {
return chapter.get("page");
};
/* Method 2: This sorts by page number in reverse */
chapters.comparator = function(chapter) {
return -chapter.get("page");
};
chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));
The following worked well for me:
comparator: function(a, b) {
// Optional call if you want case insensitive
name1 = a.get('name').toLowerCase();
name2 = b.get('name').toLowerCase();
if name1 < name2
ret = -1;
else if name1 > name2
ret = 1;
else
ret = 0;
if this.sort_dir === "desc"
ret = -ret
return ret;
}
collection.sort_dir = "asc";
collection.sort(); // returns collection in ascending order
collection.sort_dir = "desc";
collection.sort(); // returns collection in descending order
I read some where that the Backbone comparator function either uses or mimics the JavaScript Array.prototype.sort() function. This means it uses either 1, -1 or 0 to decide on the sort order of each model in the collection. This constantly updates and the collection stays in the correct order based on the comparator.
Info on Array.prototype.sort() can be found here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FArray%2Fsort
Many answers on this question simply reverse the order just before rending it to the page. This is not ideal.
Using the above article on Mozilla as a guide I was able to keep my collection sorted in reverse order using the following code, then we simply render the collection to page in it's current order and we can use a flag (reverseSortDirection) for reversing the order.
//Assuming this or similar will be in your Backbone.Collection.
sortKey: 'id', //The field name of the item in your collection
reverseSortDirection: false,
comparator: function(a, b) {
var sampleDataA = a.get(this.sortKey),
sampleDataB = b.get(this.sortKey);
if (this.reverseSortDirection) {
if (sampleDataA > sampleDataB) { return -1; }
if (sampleDataB > sampleDataA) { return 1; }
return 0;
} else {
if (sampleDataA < sampleDataB) { return -1; }
if (sampleDataB < sampleDataA) { return 1; }
return 0;
}
},
Comparator except's two models, on changes to the collection or model the compartor function runs and changes the order of the collection.
I have tested this approach with Numbers and Strings and it seems to be working perfectly for me.
I really hope this answer can help as I have struggled with this problem many times and usually end up using a work around.
This can be done elegantly by overriding sortBy method. Here is an example
var SortedCollection = Backbone.Collection.extend({
initialize: function () {
// Default sort field and direction
this.sortField = "name";
this.sortDirection = "ASC";
},
setSortField: function (field, direction) {
this.sortField = field;
this.sortDirection = direction;
},
comparator: function (m) {
return m.get(this.sortField);
},
// Overriding sortBy (copied from underscore and just swapping left and right for reverse sort)
sortBy: function (iterator, context) {
var obj = this.models,
direction = this.sortDirection;
return _.pluck(_.map(obj, function (value, index, list) {
return {
value: value,
index: index,
criteria: iterator.call(context, value, index, list)
};
}).sort(function (left, right) {
// swap a and b for reverse sort
var a = direction === "ASC" ? left.criteria : right.criteria,
b = direction === "ASC" ? right.criteria : left.criteria;
if (a !== b) {
if (a > b || a === void 0) return 1;
if (a < b || b === void 0) return -1;
}
return left.index < right.index ? -1 : 1;
}), 'value');
}
});
So you can use it like this:
var collection = new SortedCollection([
{ name: "Ida", age: 26 },
{ name: "Tim", age: 5 },
{ name: "Rob", age: 55 }
]);
//sort by "age" asc
collection.setSortField("age", "ASC");
collection.sort();
//sort by "age" desc
collection.setSortField("age", "DESC");
collection.sort();
This solution does not depend on the field type.
// For numbers, dates, and other number-like things
var things = new Backbone.Collection;
things.comparator = function (a, b) { return b - a };
// For strings
things.comparator = function (a, b) {
if (a === b) return 0;
return a < b ? -1 : 1;
};
Here is a really simple solution, for those who simply want to flip the current order. It is useful if you have a table whose columns can be ordered in two directions.
collection.models = collection.models.reverse()
You can combine it with something like this if you are interested in the table case (coffeescript) :
collection.comparator = (model) ->
model.get(sortByType)
collection.sort()
if reverseOrder
collection.models = collection.models.reverse()
For reverse order on id:
comparator: function(object) { return (this.length - object.id) + 1; }
I had a slightly different requirement, in that I am displaying my models in a table, and I wanted to be able to sort them by any collumn (model property) and either ascending or descending.
It has taken a fair amount of mucking around to get all the cases working (where values can be strings, empty strings, dates, numbers. However I think this is pretty close.
inverseString: function(str)
{
return str.split("").map(function (letter) { return String.fromCharCode(-(letter.charCodeAt(0))); }).join("");
}
getComparisonValue: function (val, desc)
{
var v = 0;
// Is a string
if (typeof val === "string")
{
// Is an empty string, upgrade to a space to avoid strange ordering
if(val.length === 0)
{
val = " ";
return desc ? this.inverseString(val) : val;
}
// Is a (string) representing a number
v = Number(val);
if (!isNaN(v))
{
return desc ? -1 * v : v;
}
// Is a (string) representing a date
v = Date.parse(val);
if (!isNaN(v))
{
return desc ? -1 * v : v;
}
// Is just a string
return desc ? this.inverseString(val) : val;
}
// Not a string
else
{
return desc ? -1 * val : val;
}
},
use:
comparator: function (item)
{
// Store the collumn to 'sortby' in my global model
var property = app.collections.global.get("sortby");
// Store the direction of the sorting also in the global model
var desc = app.collections.global.get("direction") === "DESC";
// perform a comparison based on property & direction
return this.getComparisonValue(item.get(property), desc);
},
I just overrode the sort function to reverse the models array upon completion. Also I held off triggering the 'sort' event until after the reverse has been completed.
sort : function(options){
options = options || {};
Backbone.Collection.prototype.sort.call(this, {silent:true});
this.models.reverse();
if (!options.silent){
this.trigger('sort', this, options);
}
return this;
},
Recently ran into this issue and just decided to add an rsort method.
Collection = Backbone.Collection.extend({
comparator:function(a, b){
return a>b;
},
rsort:function(){
var comparator = this.comparator;
this.comparator = function(){
return -comparator.apply(this, arguments);
}
this.sort();
this.comparator = comparator;
}
});
Hopefully someone will find this useful
Here's a quick and easy way to reverse-sort a collection's models using UnderscoreJS
var reverseCollection = _.sortBy(this.models, function(model) {
return self.indexOf(model) * -1;
});
精彩评论