Search through json with keywords - javascript

I need some help with a small thing I am struggeling with. I have to create a general search input that searches though a json of music numbers. The user has to be able to type an album/track or artist in the searchbar and then he'll get the result. Like any other search bar does. Only this one searches based on keypresses instead of a submit button.
The part where I'm stuck is that I've received a large JSON file with more than 5000 entries. And my search bar has to be able to identify entries based on partially typed "keywords". So for instance if I want to search for madonna and I type in "mado" I should already get some madonna in my results ( of course its possible to get other entries that have mado in their title or someting! ).
Sorry for my lack of good grammar but I try my best to explain the situation as good as possible!
Now for the question! The thing I'm struggeling with is how I loop through a json file to search for these keywords. This is a small portion of the json I receive:
{
"1": {
"track": "Dani California",
"artist": "Red Hot Chili Peppers",
"album": "Stadium Arcadium"
},
"2": {
"track": "Tell me baby",
"artist": "Red Hot Chili Peppers",
"album": "Stadium Arcadium"
},
"3": {
"track": "Snow (Hey Oh)",
"artist": "Red Hot Chili Peppers",
"album": "Stadium Arcadium"
}}
Normally I would create a function that is something like this:
for(var i = 0; i < data.length; i++){
if(data[i].album == 'red hot'){
console.log(data[i].album)
}}
But in this case I want to loop through the json, looking for enties that contain the search value an save it to an object for later usage
Is it possible to do this all at once? So to check the artist/title/album at once, or would it be better to create a small filter?
If something is not clear about my explanation please met me know I tried my best to be as clear as I could be!

I dont think searching 5000 entries should cause performance issues.
Check out this code which should return the desired entries when you call search('text')
var data = JSON.parse('JSON DATA HERE') // dataset
var search_fields = ['track','artist','album'] //key fields to search for in dataset
function search(keyword){
if(keyword.length<1) // skip if input is empty
return
var results = []
for(var i in data){ // iterate through dataset
for(var u=0;u<search_fields.length;u++){ // iterate through each key in dataset
var rel = getRelevance(data[i][search_fields[u]],keyword) // check if there are matches
if(rel==0) // no matches...
continue // ...skip
results.push({relevance:rel,entry:data[i]}) // matches found, add to results and store relevance
}
}
results.sort(compareRelevance) // sort by relevance
for(i=0;i<results.length;i++){
results[i] = results[i].entry // remove relevance since it is no longer needed
}
return results
}
function getRelevance(value,keyword){
value = value.toLowerCase() // lowercase to make search not case sensitive
keyword = keyword.toLowerCase()
var index = value.indexOf(keyword) // index of the keyword
var word_index = value.indexOf(' '+keyword) // index of the keyword if it is not on the first index, but a word
if(index==0) // value starts with keyword (eg. for 'Dani California' -> searched 'Dan')
return 3 // highest relevance
else if(word_index!=-1) // value doesnt start with keyword, but has the same word somewhere else (eg. 'Dani California' -> searched 'Cali')
return 2 // medium relevance
else if(index!=-1) // value contains keyword somewhere (eg. 'Dani California' -> searched 'forn')
return 1 // low relevance
else
return 0 // no matches, no relevance
}
function compareRelevance(a, b) {
return b.relevance - a.relevance
}

Since it's not an array you can't use Array.prototype.filter() unless you turn your object into an array. You could do this every time you get a new Json, no need to do this with every search.
var myArray = [];
for(var elementName in data) //We iterate over the Object to get the names of the nested objects
myArray.push(data[elementName]); //We get the objects of the json and push them inside our array.
Then you can use .filter to filter your data, I recommend using regex:
var userQuery = 'Mado' //user input
var myRegex = new RegExp('.*' + userQuery + '.*','gi'); //We create a new regular expression, this one tests if the text is contained within a string.
var filteredArray = myArray.filter(function(item){
//We test each element of the object to see if one string matches the regexp.
return (myRegex.test(item.track) || myRegex.test(item.artist) || myRegex.test(item.album))
});
filteredArray should be the elements of the json you need.
Array.prototype.filter MDN
RegeExp MDN

Here's a pattern I often use for filter functionalities. Some key points are:
Always build an index property that contains the appended string of the filterable values. For example, if the values of 'track','artist' and 'album' can be filtered on, then join their values into a string, and add that string as one of the properties to the original object.
This helps quickly search using indexOf, rather than having to iterate through each object when filtering. It significantly improves the performance as those iterations and additional number of properties*number of objects comparisons are no longer required. In your case, you'd be saving roughly 10k comparisons and 15k iterations with every filter operation.
If the filter operation is case-insensitive, use toLowerCase while appending the values to build indexes. It also saves you from performing those many toLowerCase operations every time filter is called.
Always create an array of objects, rather than an object with object properties. I don't have specific stats on whether this improves performance or not, but it provides you some array methods such as array.filter or array.sort that you could utilize to improve UX. I haven't done that in the snippet, but you can do that quite easily while preparing the data.
var data = {
"1": {
"track": "Dani California",
"artist": "Red Hot Chili Peppers",
"album": "Stadium Arcadium"
},
"2": {
"track": "Tell me baby",
"artist": "Red Hot Chili Peppers",
"album": "Stadium Arcadium"
},
"3": {
"track": "Snow (Hey Oh)",
"artist": "Red Hot Chili Peppers",
"album": "Stadium Arcadium"
}};
// One time activity!
// Build search indexes, for every object.
for(var prop in data) {
if(data.hasOwnProperty(prop)) {
var index = "";
var item = data[prop];
// Iterate over each object and build the index by appending the values of each property.
for(var attr in item) {
if(item.hasOwnProperty(attr)) {
// Note: Different values are separated by a hash as hash # is unlikely to come into the search query.
index = index + item[attr] + "#";
}
}
// Insert the index property into the object.
// Also notice the toLowerCase that allows for case insenstive searches later on.
item.index = index.toLowerCase();
}
}
console.log("Prepared data:" ,data);
// Filter process.
var key = "Sn";
var keyLowerCase = key.toLowerCase();
// Iterate over the objects and compare the index prpoerty to match with the search string.
var filteredData = [];
for(var prop in data) {
if(data.hasOwnProperty(prop)) {
var item = data[prop];
if(item.index.indexOf(keyLowerCase) >= 0 ){
filteredData.push(item);
}
}
}
console.log("Filtered data:", filteredData);

Related

Is there a way to iterate and display the list of key value pairs of json using Handlebar.js

As Iam new to javascript, I found handleBar.js can be used to template with dynamic data.
I worked on a sample which worked fine and the json structure was simple and straight forward.
(function()
{
var wtsource = $("#some-template").html();
var wtTemplate = Handlebars.compile(wtsource);
var data = { users: [
{url: "index.html", name: "Home" },
{url: "aboutus.html", name: "About Us"},
{url: "contact.html", name: "Contact"}
]};
Handlebars.registerHelper('iter', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var ret = "";
if(context && context.length > 0) {
for(var i=0, j=context.length; i<j; i++) {
ret = ret + fn($.extend({}, context[i], { i: i, iPlus1: i + 1 }));
}
} else {
ret = inverse(this);
}
return ret;
});
var temp=wtTemplate(data);
$("#content").html(temp);
})();
<script id="some-template" type="text/x-handlebars-template">
{{#iter users}}
<li>
{{name}}
</li>
{{/iter}}
</script>
How to iterate a json with the below structure ? Please do suggest the possible way for iterating and creating the template for the below json structure
var newData = { "NEARBY_LIST": {
"100": {
"RestaurantID": 100,
"ParentRestaurantID": 0,
"RestaurantName": "Chennai Tiffin",
"listTime": [{
"startTime": "10:00",
"closeTime": "23:30"
} ]
},
"101": {
"RestaurantID": 101,
"ParentRestaurantID": 0,
"RestaurantName": "Biriyani Factory",
"listTime": [{
"startTime": "11:00",
"closeTime": "22:00"
}]
}
}
};
Accessing the properties of an object has nothing to do with Handlebars. If you dealing with JSON and you wish to access it in general bracket or dot notation, you must first parse the JSON into a JavaScript object using the JSON.parse() function.
After this is done, you may access the properties as follows.
var property = newData['NEARBY_LIST']['100'].RestaurantName; // "Chennai Tiffin"
Here is a fiddle to illustrate.
http://jsfiddle.net/qzm0cygu/2/
I'm not entirely sure what you mean, but if your question is how you can use/read the data in newData, try this:
newData = JSON.parse(newData); //parses the JSON into a JavaScript object
Then access the object like so:
newData.NEARBY_LIST //the object containing the array
newData.NEARBY_LIST[0] //the first item (key "100")
newData.NEARBY_LIST[1] //the second item (key "101")
newData.NEARBY_LIST[0][0] //the first field of the first item (key "RestaurantID", value "100")
newData.NEARBY_LIST[0][2] //the third field of the first item (key "RestaurantName", value "Chennai Tiffin")
newData.NEARBY_LIST[0][3][0] //the first field of the fourth field of the first item (key "startTime", value "11:00")
I hope this was what you were looking for.
EDIT: as Siddharth points out, the above structure does assume you have arrays. If you are not using arrays you can access the properties by using their names as if they're in an associative array (e.g. newData["NEARBY_LIST"]["100"]. The reason I say "properties" and "as if" is because technically JavaScript doesn't support associative arrays. Because they are technically properties you may also access them like newData.NEARBY_LIST (but I don't recommend that in this case as a property name may not start with a number, so you would have to use a mix of the different notations).
On that note, I would recommend using arrays because it makes so many things easier (length checks, for example), and there are practically no downsides.
EDIT2: also, I strongly recommend using the same camelcasing conventions throughout your code. The way you currently have it (with half your properties/variables starting with capitals (e.g. "RestaurantName", "RestaurantID") and the other half being in lowerCamelCase (e.g. "listTime", "startTime")) is just asking for people (you or colleagues) to make mistakes.

Using concatenation and a passed parameter to loop through an array

var Animals = {
"Europe": { "weasel.jpg": "squeak", "cow.jpg": "moo"},
"Africa": { "lion.jpg": "roar", "gazelle.jpg": "bark"},
};
function region(a){
var b = "Animals."+a;
for(var index in b) {
var target = document.getElementById('div1');
var newnode = document.createElement('img');
newnode.src = index;
target.appendChild(newnode)
}
}
RELEVANT HTML
<li onclick="europe('Europe')">Europe</li>
Goal: on the click of the Europe <li>, pass the word Europe into my region function where it is then concatenated to produce Animals.Europe
This is in order to identify an array within the object structure at the top using the for(var index in Animals.Europe) loop. Why is the concatenation which produces Animals.Europe not treated in the same way as if I had typed this out?
In addition, you can see that I have used arrays to store an image source and description for different animals. Using my limited coding knowledge this was all I could think of. Is there an easier way to store image/description data in order to produce in HTML?
"Animals." + a is just a string value, e.g. "Animals.Europe", which is not the same thing as Animals.Europe. If you change the first line to var b = Animals[a];, you should be all set.
Edit: and as elclanrs pointed out, it should be region('Europe'), not europe('Europe').
Why is the concatenation which produces Animals.Europe not treated in the same way as if i had typed this out?
In this case the variable b is just a string ("Animals.Europe"), which is treated like any other string (i.e. a list of characters). This means that when you attempt to loop through it (for(index in b)) you will be looping over a simple list of characters.
What you can do instead is use the square brace notation of accessing an objects properties. This means you can instead write var b = Animals[a], retrieving attribute a from Animals. You can read more about working with objects in this way on this MDN page
You can access the europe property using the following
Animals[a]
Also you're calling a "europe" function when you should be calling "region"
You're not storing animals in arrays here, but in objects with the image names as keys. Usually you'll want to use relevant names as keys. For example if you want arrays of animals for each continent
var Animals = {
"Europe": [{
imageSrc: "weasel.jpg",
cry: "squeak"
},{
imageSrc: "cow.jpg",
cry: "moo"
}],
"Africa": [{
imageSrc: "lion.jpg",
cry: "roar"
},{
imageSrc: "gazelle.jpg",
cry: "bark"
}]
};
Now Animals['Europe'] gives an array of objects, where you could eventually store other properties. So if b is an array your loop will now look like:
var b = Animals['Europe'];
for(var i=0; i < b.length; i++) {
var target = document.getElementById('div1');
var newnode = document.createElement('img');
var animalData = b[i]; // The array item is now an object
newnode.src = animalData.imageSrc;
target.appendChild(newnode)
}

Backbone.js - Filter a Collection based on an Array containing multiple keywords

I'm using Backbone.js/Underscore.js to render a HTML table which filters as you type into a textbox. In this case it's a basic telephone directory.
The content for the table comes from a Collection populated by a JSON file.
A basic example of the JSON file is below:
[{
"Name":"Sales and Services",
"Department":"Small Business",
"Extension":"45446",
},
{
"Name":"Technical Support",
"Department":"Small Business",
"Extension":"18800",
},
{
"Name":"Research and Development",
"Department":"Mid Market",
"Extension":"75752",
}]
I convert the text box value to lower case and then pass it's value along with the Collection to this function, I then assign the returned value to a new Collection and use that to re-render the page.
filterTable = function(collection, filterValue) {
var filteredCollection;
if (filterValue === "") {
return collection.toJSON();
}
return filteredCollection = collection.filter(function(data) {
return _.some(_.values(data.toJSON()), function(value) {
value = (!isNaN(value) ? value.toString() : value.toLowerCase());
return value.indexOf(filterValue) >= 0;
});
});
};
The trouble is that the function is literal. To find the "Sales and Services" department from my example I'd have to type exactly that, or maybe just "Sales" or "Services". I couldn't type "sal serv" and still find it which is what I want to be able to do.
I've already written some javascript that seems pretty reliable at dividing up the text into an array of Words (now updated to code in use).
toWords = function(text) {
text = text.toLowerCase();
text = text.replace(/[^A-Za-z_0-9#.]/g, ' ');
text = text.replace(/[\s]+/g, ' ').replace(/\s\s*$/, '');
text = text.split(new RegExp("\\s+"));
var newsplit = [];
for (var index in text) {
if (text[index]) {
newsplit.push(text[index]);
};
};
text = newsplit;
return text;
};
I want to loop through each word in the "split" array and check to see if each word exists in one of the key/values. As long as all words exist then it would pass the truth iterator and get added to the Collection and rendered in the table.
So in my example if I typed "sal serv" it would find that both of those strings exist within the Name of the first item and it would be returned.
However if I typed "sales business" this would not be returned as although both the values do appear in that item, the same two words do not exist in the Name section.
I'm just not sure how to write this in Backbone/Underscore, or even if this is the best way to do it. I looked at the documentation and wasn't sure what function would be easiest.
I hope this makes sense. I'm a little new to Javascript and I realise I've dived into the deep-end but learning is the fun part ;-)
I can provide more code or maybe a JSFiddle if needed.
Using underscore's any and all make this relatively easy. Here's the gist of it:
var toWords = function(text) {
//Do any fancy cleanup and split to words
//I'm just doing a simple split by spaces.
return text.toLowerCase().split(/\s+/);
};
var partialMatch = function(original, fragment) {
//get the words of each input string
var origWords = toWords(original + ""), //force to string
fragWords = toWords(fragment);
//if all words in the fragment match any of the original words,
//returns true, otherwise false
return _.all(fragWords, function(frag) {
return _.any(origWords, function(orig) {
return orig && orig.indexOf(frag) >= 0;
});
});
};
//here's your original filterTable function slightly simplified
var filterTable = function(collection, filterValue) {
if (filterValue === "") {
return collection.toJSON();
}
return collection.filter(function(data) {
return _.some(_.values(data.toJSON()), function(value) {
return partialMatch(value, filterValue);
});
});
};
Note: This method is computationally pretty inefficient, as it involves first looping over all the items in the collection, then all the fields of each item, then all words in that item value. In addition there are a few nested functions declared inside loops, so the memory footprint is not optimal. If you have a small set of data, that should be OK, but if needed, there's a number of optimizations that can be done. I might come back later and edit this a bit, if I have time.
/code samples not tested

localstorage: Get specific localstorage value of which contains many items

In localstorage I have key 'results' with this values:
[{"id":"item-1","href":"google.com","icon":"google.com"},
{"id":"item-2","href":"youtube.com","icon":"youtube.com"},
{"id":"item-3","href":"google.com","icon":"google.com"},
{"id":"item-4","href":"google.com","icon":"google.com"},
{"id":"item-5","href":"youtube.com","icon":"youtube.com"},
{"id":"item-6","href":"asos.com","icon":"asos.com"},
{"id":"item-7","href":"google.com","icon":"google.com"},
{"id":"item-8","href":"mcdonalds.com","icon":"mcdonalds.com"}]
To get the last item I use this:
// this is how I parse the arrays
var result = JSON.parse(localStorage.getItem("result"));
for(var i=0;i<result.length;i++) {
var item = result[i];
$('element').val(item.href);
}
How can I get the href for item-3 or for a specific ID?
Using native Array.filter
If you are targeting only modern browsers (IE9+ or a recent version of any other major browser) you can use the JavaScript 1.6 array method filter.
var testItem,
data = [{"id":"item-1","href":"google.com","icon":"google.com"},
{"id":"item-2","href":"youtube.com","icon":"youtube.com"},
{"id":"item-3","href":"google.com","icon":"google.com"},
{"id":"item-4","href":"google.com","icon":"google.com"},
{"id":"item-5","href":"youtube.com","icon":"youtube.com"},
{"id":"item-6","href":"asos.com","icon":"asos.com"},
{"id":"item-7","href":"google.com","icon":"google.com"},
{"id":"item-8","href":"mcdonalds.com","icon":"mcdonalds.com"}];
function getItemById(data, id) {
// filter array down to only the item that has the id
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter
var ret = data.filter(function (item) {
return item.id === id;
});
// Return the first item from the filtered array
// returns undefined if item was not found
return ret[0];
}
testItem = getItemById(data, 'item-3');
Working example
Manually looping over the data
If you can't use filter you are probably stuck with just using a loop:
var testItem,
data = [{"id":"item-1","href":"google.com","icon":"google.com"},
{"id":"item-2","href":"youtube.com","icon":"youtube.com"},
{"id":"item-3","href":"google.com","icon":"google.com"},
{"id":"item-4","href":"google.com","icon":"google.com"},
{"id":"item-5","href":"youtube.com","icon":"youtube.com"},
{"id":"item-6","href":"asos.com","icon":"asos.com"},
{"id":"item-7","href":"google.com","icon":"google.com"},
{"id":"item-8","href":"mcdonalds.com","icon":"mcdonalds.com"}];
function getItemById(data, id) {
var i, len;
for (i = 0, len = data.length; i < len; i += 1) {
if(id === data[i].id) {
return data[i];
}
}
return undefined;
}
testItem = getItemById(data, 'item-3');
Working example
Even though brute-forcing it with a loop might seem less elegant than using Array.filter, it turns out that in most cases the loop is faster than Array.filter.
Refactoring to an object instead of an array
The best solution, assuming that the id of each of your items is unique, would be refactoring the way you are storing the data. Instead of an array of objects, use an object that uses the id as a key to store an object containing the href and icon key/property values.
var data = {
"item-1": {"href": "google.com", "icon": "google.com"},
"item-2": {"href": "youtube.com", "icon": "youtube.com"},
"item-3": {"href": "google.com", "icon": "google.com"},
"item-4": {"href": "google.com", "icon": "google.com"},
"item-5": {"href": "youtube.com", "icon": "youtube.com"},
"item-6": {"href": "asos.com", "icon": "asos.com"},
"item-7": {"href": "google.com", "icon": "google.com"},
"item-8": {"href": "mcdonalds.com", "icon": "mcdonalds.com"}
};
This would make accessing items even easier and faster:
var data = JSON.parse(localStorage.getItem("result"));
data["item-3"].href;
jQuery has filter helper for that:
$(result).filter(function(){return this.id == "item-3";})[0]
Function for href of item with specific id would be:
function getItemHrefById(json, itemId){
return json.filter(function(testItem){return testItem.id == itemId;})[0].href;
}
And sample usage is:
var href = getItemHrefById(result, "item-3");
You can see working example on http://jsfiddle.net/LXvLB/
UPDATE
If you cannot read item from local storage, maybe you forgot to call JSON.stringify when setting value:
localStorage["results"] = JSON.stringify([{"id":"item-1","href":"google.com","icon":"google.com"},
{"id":"item-2","href":"youtube.com","icon":"youtube.com"},
{"id":"item-3","href":"google.com","icon":"google.com"},
{"id":"item-4","href":"google.com","icon":"google.com"},
{"id":"item-5","href":"youtube.com","icon":"youtube.com"},
{"id":"item-6","href":"asos.com","icon":"asos.com"},
{"id":"item-7","href":"google.com","icon":"google.com"},
{"id":"item-8","href":"mcdonalds.com","icon":"mcdonalds.com"}])
You need to convert json to string to be properly serialized (and to use JSON.parse to get JSON back)
This is final example.
EDIT
As Useless Code pointed out, this metod is substantially slower than native filter function (and custom loop but I think that introducing few new lines of code to save 20-30ms is overkill unless performance is a issue), so I'm updating my example to not use jquery filter. +1 please for his answer for that.
Also, what is important to point out here, if this array would have hundreds instead of 8 bookmarks, for loop would probably be statistically about twice faster (as it does not have to iterate through rest of the array). But, in that case it would probably be a good idea to put for loop into function which returns first found item which satisfies condition, and with prototypejs it probably can even be hooked up to array.
for the jquery filter method, I think using a callback function, and bind the search parameter is more elegant and readable:
function filterById(id, i, obj) {
return obj.id === id;
}
function getItemHrefById(json, itemId) {
return $(json).filter(filterById.bind(null, itemId))[0].href;
}
da usual fiddle
(however, i prefer the "for loop" approach" for this!!)

Sorting strings in reverse order with backbone.js

I'm trying to sort a Backbone.js collection in reverse order. There are previous replies on how to do this with integers, but none with strings.
var Chapter = Backbone.Model;
var chapters = new Backbone.Collection;
chapters.comparator = function(chapter) {
return chapter.get("title");
};
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 above code sorts the chapters from A -> Z, but how do I write a comparator that sorts it from Z -> A?
You could:
grab the char code for each character in the string,
subtract each value from 0xffff (the maximum return value of string.charCodeAt),
use String.fromCharCode to turn that back into string of "negated" characters
and that will be your sorting key.
chapters.comparator = function(chapter) {
return String.fromCharCode.apply(String,
_.map(chapter.get("title").split(""), function (c) {
return 0xffff - c.charCodeAt();
})
);
}
And voila:
> console.log(chapters.pluck('title'));
["The Middle", "The End", "The Beginning"]
Note: if your comparison strings are long (as in 65 kb or more), you may run into trouble (see Matt's comment below). To avoid this, and speed up comparisons a bit, just use a shorter slice of your comparison string. (In the above example, you could go for chapter.get("title").slice(0, 100).split("") instead.) How long a slice you need will depend on your application.
There are two versions of the comparator function that you can use, either the sortBy version - which was shown in the example, which takes one parameter, or sort - which you can return a more standard sort function, which the documentation says:
"sortBy" comparator functions take a model and return a numeric or string value by which the model should be ordered relative to others. "sort" comparator functions take two models, and return -1 if the first model should come before the second, 0 if they are of the same rank and 1 if the first model should come after.
So in this case, we can write a different comparator function:
var Chapter = Backbone.Model;
var chapters = new Backbone.Collection;
chapters.comparator = function(chapterA, chapterB) {
if (chapterA.get('title') > chapterB.get('title')) return -1; // before
if (chapterB.get('title') > chapterA.get('title')) return 1; // after
return 0; // equal
};
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'));
So you should get as a response:
"The Middle", "The End", "The Beginning"
If you're working with non-numerical values, there is no obvious way to do a reverse sort. Backbone makes use of the _.sortBy() and _.sortedIndex() methods from Underscore to order the models based on the comparator, and these methods automatically sort in ascending order. The naive way to do this would be to use chapters.pluck('title').reverse(), as the result of pluck will be an array. But calling reverse on some Collection methods will reverse the Collection models in place, so next time you call it, the models will be back in ascending order. You could always do something like:
var results = [],
titles = chapters.pluck('title');
for(var i=0, len=titles.length; i<len; i++) {
results.push(titles[i]);
}
results.reverse();
This would not affect the models array in your Backbone collection, as it would create a completely new results array in memory, but retain references to the original models, so calling things like save would still update the Collection state.
But that's not very elegant, and creates a lot of extra coding throughout your project any time you want to reverse the results. I think we can do better.
In order to make this work, you'll need to perform a bit of unwieldy JavaScript ninjary in your comparator method to make this work - note this is untested:
chapters.comparator = function(chapter) {
var alphabet = '0123456789abcdefghijklmnopqrstuvwxyz',
title = chapter.get('title').toLowerCase(),
inverse_title = '',
index;
for(var i=0, len=title.length; i<len; i++) {
index = alphabet.indexOf(title.charAt(i));
if(index === -1) {
inverse_title += title.charAt(i);
continue;
}
inverse_title += alphabet.charAt(alphabet.length - index - 1);
}
return inverse_title;
};
This concept probably needs improving to take into account symbols, etc., but essentially it inverts the comparator string in such a way that "Z" becomes "0", "Y" becomes "1", etc., which should produce the reverse sort you're after.
As Backbone merely uses the .sortBy method, simply proxy in your own logic:
collectionInQuestion.sortBy = function () {
var models = _.sortBy(this.models, this.comparator);
if (forSomeReason) {
models.reverse();
}
return models;
};
..or add it somewhere else..
TweakedCollection = Backbone.Collection.extend({ sortBy: [...] })
I just solved a similar problem with table sorting and I wanted to share the code since I didn't find much help in these answers:
events: {
'click th.sortable': function(e) {
var $this = $(e.target),
order = $this.hasClass('asc') ? 'desc' : 'asc',
field = $this.data('field'); /* this is a string */
$this.siblings().addBack().removeClass('asc desc');
$this.addClass( order );
this.bodyView.collection.comparator = field;
this.bodyView.collection.sort();
if ( order === 'desc' ) this.bodyView.collection.models.reverse();
this.bodyView.render();
}
},
in this case I simply set comparator to string instead of a function; the string has to be the name of the property you want to sort by.
Then I just call reverse on the models if the order has to be inverse.
Just add minus before chapter.get
chapters.comparator = function(chapter) {
    return -chapter.get("title");
};

Categories