sort backbone collection based on model attributes - javascript

I have a backbone collection which is rendered in a table. I would like to make the table sortable based on certain attributes the collection has, like "task_status","task_group". I've being reading the backbone documentation about collection.comparator,nd collection.sort.
How can I get this done?

The comparator function is used to compare two models in the collection and it can compare them in any (consistent) way that it wants to. In particular, it can choose which model attribute to use so you could have something like this in your collection:
initialize: function() {
this.sort_key = 'id';
},
comparator: function(a, b) {
// Assuming that the sort_key values can be compared with '>' and '<',
// modifying this to account for extra processing on the sort_key model
// attributes is fairly straight forward.
a = a.get(this.sort_key);
b = b.get(this.sort_key);
return a > b ? 1
: a < b ? -1
: 0;
}
and then you just need some methods on the collection to change the sort_key and call sort:
sort_by_thing: function() {
this.sort_key = 'thing';
this.sort();
}
In older Backbones, calling sort will trigger a "reset" event whereas newer versions will trigger a "sort" event. To cover both cases you can listen to both events and re-render:
// in the view...
initialize: function() {
this.collection.on('reset sort', this.render, this);
}
Demo: http://jsfiddle.net/ambiguous/7y9CC/
You can also use listenTo instead of on to help you avoid zombies:
initialize: function() {
this.listenTo(this.collection, 'reset sort', this.render);
}
Demo: http://jsfiddle.net/ambiguous/nG6EJ/

#mu-is-too-short's answer is good, except there's an easier way to compare the field values:
The easiest way to sort the collection based on a field, is to provide a comparator function that returns the exact field's value you want to sort by. This kind of comparator causes Backbone to call sortBy function, instead of sort, which then does that complex comparison on it's own and you don't have to worry about the logic.
So in essence, you don't have to provide a complex comparator function, unless you have a more advanced need for determining the order.
var myCollection = Backbone.Collection.extend({
sort_key: 'id', // default sort key
comparator: function(item) {
return item.get(this.sort_key);
},
sortByField: function(fieldName) {
this.sort_key = fieldName;
this.sort();
}
});
After this you can just call the collection's sortByField -function with a string that represents the key that you want to sort by.
For example:
collection.sortByField('name');
Modified #my-is-too-short's demo: http://jsfiddle.net/NTez2/39/

#jylauril's answer helps out tremendously, but needed to modify the demo (perhaps slight changes in backbone since it was posted?)
Looks like you need to trigger a render after you've sorted.
$('#by-s').click(function() {
c.sortByField('s');
v.render();
});
Updated #my-is-too-short's demo: http://jsfiddle.net/NTez2/13/

Related

Knockout.js two way binding between multi select and delimited string

Just discovering knockout so please point out stupidity if I missed something here. I currently have a multiple select bound to an observable in knockout.
<select class="form-control" id="SithSelect" data-bind="attr: { 'data-has-value': (Sith() ? true : false) }, selectedOptions: Sith() ? Sith().split('|') : '' " placeholder="The dark side" multiple>
<option disabled></option>
<option value="Darth Vader">Darth Vader</option>
<option value="Darth Maul">Darth Maul</option>
<option value="Darth Bane">Darth Bane</option>
</select>
Where "Sith" is an observable string value.
When I manually add the string value "Darth Vader|Darth Maul" in the database and the value eventually gets down to the client side, the multi select correctly shows two values selected.
However, if I change the values, how do I then combine the selected values back to a pipe delimited string stored in the observable?
From what I've tried I can sort this by having an observable array that the select is bound to and then re-construct the pipe delimited string on save to the database. This solution smells a bit funny when I look over the code tho because it feels tightly coupled to the individual select element (this is the only one on the page that uses a pipe delimited string from the DB).
Have I missed something in managing to do two-way binding between a multi select and a delimited string? Rather, is there a way to do the opposite of
selectedOptions: Sith() ? Sith().split('|') : ''
when assigning the multiple selected options back to the observable value in the binding?
UPDATE: The question has been narrowed down to:
Is there something inbuilt to knockout that I can place within a binding, that can handle the read/write functions of a computed value, so that I can write it inline, rather than having to create a separate computed observable value?
selectedOptions should be array or an observable array. http://knockoutjs.com/documentation/selectedOptions-binding.html
There's no way for Knockout to know how to save data back to Sith, when you're passing to it some data calculated from Sith.
You should use ko.observableArray or just ko.observable with array value. But you'll need to synchronize your string value and array value. Instead of manually supporting two subscriptions (str=>arr, arr=>str) you can use writable computed (http://knockoutjs.com/documentation/computed-writable.html):
<select data-bind="selectedOptions: sith">...</select>
vm.sithString = ko.observable('');
vm.sith = ko.computed({
read: function() { return vm.sithString().split('|'); },
write: function(arr) { vm.sithString(arr.join('|')); }
});
UPDATE
Is there something inbuilt to knockout that I can place within a binding, that can handle the read/write functions of a computed value, so that I can write it inline, rather than having to create a separate computed observable value?
This is the exact purpose of ViewModel in MVVM. You have model, which is you pure data (well, you have to store it in observables, but anyway). You have bindings, which are purely UI things. And you have mediator between them ViewModel to convert data and events between them. Writable computed in this example represents ViewModel-Model relations.
Writable computeds is much more general and flexible mechanism than write/read functions per binding.
UPDATE 2
Well, selectedOptions is not the easiest binding to sit on top of. The way is to attach exactly the same computed to element and replace valueAccessor in binding functions. Code is straightforward, but requires knowledge of how bingings work:
var selectedOptions = ko.bindingHandlers['selectedOptions'];
ko.bindingHandlers['pipedOptions'] = {
after: selectedOptions.after, /* ['options', 'foreach'] */
init: function(el, va) {
var obsv = va();
var mediator = ko.computed({
read: function() { return obsv().split('|'); },
write: function(arr) { obsv(arr.join('|')); }
});
ko.utils.domData.set(el, 'pipedOptions.mediator', mediator);
var args = [].slice.call(arguments);
args[1] = function() { return mediator; };
selectedOptions.init.apply(this, args);
},
update: function(el, va) {
var mediator = ko.utils.domData.get(el, 'pipedOptions.mediator');
var args = [].slice.call(arguments);
args[1] = function() { return mediator; };
selectedOptions.update.apply(this, args);
}
};
If you need more complex behavior take a look at components.

Iterate over a backbone collection based on properties of models in an elegant way

I have a backbone collection and based on an attribute of models inside collection I iterate over the collection and show it in the UI.
The logic is, if the model has property isNewCar as true, I'll first show them all in UI, followed by a separator then i'll show all models having property isNewCar as false.
this.cars.forEach(function (car, index)
{
if(car.isNewCar()){ //IF A NEW CAR
//some logic.
//Attach current view in DOM with this model's properties
}
});
//Here, Attach some separator in DOM
this.cars.forEach(function (car, index)
{
if(!car.isNewCar()){ //IF 'NOT' A NEW CAR
//some logic.
//Attach current view in DOM with this model's properties
}
});
This looks messy and I understand its not so elegant, can someone suggest a better way to replace above code with some elegant solution?
I would suggest have a common rendering logic to render the items in a separate function say renderCars() and filter the collection as below
function filterCars(isNew) {
var isNewCar = isNew
return function(car) {
return (car.isNewCar() === isNewCar);
}
}
renderCars (this.cars.filter(filterCars(true)) );
renderCars (this.cars.filter(filterCars(false)) );
All we have done above is created a helper function filterCars that takes a boolean to decide if we need new cars or not. This helper function returns a function that is used to filter the cars .
Backbone filter uses the underscore filter that returns a new array of the filtered results. I am passing this to the common render function.
I did not test this but this should help clear out some repeated code.

Sorting a backbone collection alphabetically

I have a backbone collection that is pulling in a bunch of template names for people to use and I would like to sort them alphabetically so their easier to find. I am very unsure of how to do this though.
I have my backbone collection
this.templates = new Backbone.Collection();
and then I'm sorting through the templates to figure out where to add what.
var Names = this.model.collection.models.map(function(model){
return (model.attributes.Name) ? model.attributes.Name : 'Template';
});
Names.forEach(function(name) {
_this.templates.add(api.collections[(_this.templateType)].where({Name : name, ShowInToolBox : true}));
//adding a bunch of conditionals to add cretin forms to modules that are outside the scope
}
Is it possible to alphabetize these?
I've tried adding .sortBy("Name") to the backbone collection, but it just stopped my code from running.
Backbone offers the comparator property for sorting. You can pass the name of the property that the collection should be sorted on into the constructor:
this.templates = new Backbone.Collection([], { comparator: 'Name' })
Every time the collection changes, it will be re-sorted by the property name in the comparator. If you're doing something more complicated, you can define the comparator as a function. If you go this route, then I would recommend extending Backbone.Collection for clarity:
var Templates = Backbone.Collection.extend({
comparator: function(template1, template2){
return template1.get('someValue') - template2.get('someValue')
}
})
var templates = new Templates()
Backbone collections can be sorted with the comparator function.
If you define a comparator, it will be used to maintain the collection in sorted order. This means that as models are added, they are inserted at the correct index in collection.models. A comparator can be defined as a sortBy (pass a function that takes a single argument), as a sort (pass a comparator function that expects two arguments), or as a string indicating the attribute to sort by.

Backbone view-property: one on instance, one on prototype?

Yeah, that title doesn't make much sense.
I have a view. This view creates sub-views. These subviews are stores in an array so that I can remove them at some point.
MN.ContactsView = MN.BaseView.extend ({
tagName : "div",
contactViewItems : [],
initialize : function(){
this.listenTo(MN.client.contacts, "add", this.addOne);
this.listenTo(MN.client.contacts, "reset", this.addAll);
this.listenTo(MN.client.contacts, "all", this.render);
MN.client.contacts.fetch();
}, render: function(){
},
addOne : function($contactModel){
var view = new MN.ContactsViewItem({model: $contactModel});
this.contactViewItems.push(view);
$("#contactsContainer").append(view.render().el);
},
addAll : function(){
MN.client.contacts.each(this.addOne, this)
},
close : function(){
},
destroy: function(){
for(var i =0; i < this.contactViewItems.length; i++) this.contactViewItems[i].destroy();
this.contactViewItems = [];
debugger;
console.log("Length: " + this.contactViewItems.length );
MN.BaseView.prototype.destroy.call(this);
},
when I destroy the view and check the debugger, I see the same variable 2 times. What is up backbone?
When you're creating your class by using the extend method, the properties you gave will be put in the prototype. That means, they'll be shared by all the instances of your class. If you happen to change the value of that property in some instance, you will shadow the prototype so that this property has a new value, not linked with the prototype anymore.
So, here's what's happening here:
When you modify contactViewItems with this.contactViewItems.push(view);, you don't change the value stored in the prototype. Rather, you change the object pointed by the value. So you're still modifying an array, shared by all the instances of your class.
However, when you do this.contactViewItems = [];, you really are changing the value, as you're giving it a totally new object. At that moment, you simply shadow the prototype of your class for this instance in particular. The prototype is of course still there (you didn't do anything to it), therefore you see the property twice.
Now, it's certainly not working as you want it to work, and there are several solutions that I'm sure you can think of now that you know the core of the problem (I don't know the details of your app, so I can't be of much help without further details).
when I destroy the view and check the debugger, I see the same variable 2 times.
Because you've created it a second time on the instance in the destroy function:
this.contactViewItems = [];
If you wanted to clear the array on your prototype, you should've used either
MN.ContactsView.prototype.contactViewItems = [];
or
this.contactViewItems.length = 0;
As Loamhoof pointed out, the issue is your contactViewItems are attributes of your ContactsViews prototype.
A simply way to modify your view so that it behaves the way you are probably expecting, is to NOT iniitilize contactViewItems with an empty array. Instead, create the empty array in your initialize.
For example:
MN.ContactsView = MN.BaseView.extend ({
tagName : "div",
contactViewItems : null,
initialize : function(){
this.contactViewItems = [];
this.listenTo(MN.client.contacts, "add", this.addOne);
this.listenTo(MN.client.contacts, "reset", this.addAll);
this.listenTo(MN.client.contacts, "all", this.render);
MN.client.contacts.fetch();
},
Now, adding items to this.contactViewItems is directly on the view object (what you probably want in this case) while the prototype contains a contactViewItems that is null.

how to sort backbone models/views?

What is the basic way to keep models any views in order using Backbone? I have some idea but it's not totally clear.
I want to keep them in order by a field called "created_at". I know there is the ability to provier a comparator function in the collection but I'm not sure how it works.
I also want this order in the collection to be reflected by the views at all times (in a list). I'm not exactly sure where I tie into the model though. I'm guessing i look for change in an index attribute and then update a to match?
Thanks very much for any help or explanation!
When you define your collection, you also define comparators.
I did it this way recently:
comparators: {
id: function(animal) {
return Number(animal.get("id"));
},
d_id: function(animal) {
return -Number(animal.get("id")); // descending
},
name: function(animal) {
return animal.get("name");
},
d_name: function(animal) {
return String.fromCharCode.apply(String, _.map(animal.get("name").split(""), function (c) {
return 0xffff - c.charCodeAt();
})
);
},
}
these I defined within my collection code.
Then, in rendering my collection views, I just did this
(this was within my view that renders the whole collection, in initialize():
this.collection = new MyCollection();
this.collection.comparator = Collection.comparators[// here I put 'id' or 'd_id' etc. ];
this.collection.sort();
Since this code is in your views's initialize, you can define your comparator
when you initialize your view, and pass it a name of a comparator like this:
var directory = new pageView("d_id");
and than thru initialize(comparator_id) you could pass this to your code in initialize:
this.collection = new MyCollection();
this.collection.comparator = Collection.comparators[comparator_id];
this.collection.sort();
And then I can use the collection in rendering and re-rendering the view/page
Edited:
Here is Backbone's collection.comparator documentation,
And right below it is an explanation of sort()
Basically, comparator can be a property of a model or a function that returns a property,
Or a negative property, if it's numeric, for descending order,
or a string or its reverse value for descending order
like in the example I gave you here.
So, comparator returns a property like "id" or "name", or "-id" , "-name" etc etc.
(for string you can't just make it "negative", you need to apply more complex function,
as I wrote.)

Categories

Resources