Backbone collection sortBy - javascript

I make my first backbone app and get some problems with collection sorting.
After using this
var SortedFriends = MyFriends.sortBy(function(friend) {
return friend.get("uid");
});
console.log(SortedFriends) show that SortedFriends contains sorted models, but when i try to use collection functions like 'SortedFriends.each' or 'SortedFriends.at' it make error:
TypeError: SortedFriends.each is not a function.
Code:
var Friend = Backbone.Model.extend({});
var Friends = Backbone.Collection.extend({
model: Friend,
});
var MyFriends = new Friends();
MyFriends.reset(<?=$friends?>);
var FriendView = Backbone.View.extend({
initialize: function(){
model:Friend
},
tagName: "tr",
template: _.template($('#item-template').html()),
className: "document-row",
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var SortedFriends = MyFriends.sortBy(function(friend) {
return friend.get("uid");
});
var addOne = function(element){
var view = new FriendView({model: element});
$("#friends").append(view.render().el);
}
console.log(JSON.stringify(SortedFriends));
SortedFriends.each(function(friend){
var view = new FriendView({model: friend});
$("#friends").append(view.render().el);
});

If youre using backbone collections then youre probably better off using the comparator rather than collection methods
http://backbonejs.org/#Collection-comparator
When youre ready to sort your collection:
MyFriends.comparator = function(friend){
return friend.get("uid");
});
MyFriends.sort();
OR if you want to keep the order of the unsorted collection then you will need to clone it first
http://backbonejs.org/#Collection-clone
var SortedFriends = MyFriends.clone();
SortedFriends.comparator = function(friend){
return friend.get("uid");
});
SortedFriends.sort();

I'm not sure if it's a bug or a feature of Backbone's adaptation of sortBy, but apparently it returns an array, not an Underscore collection.
One workaround is to wrap the whole thing in _( ... ), which tells Underscore to wrap the array back into a collection:
var SortedFriends = _(MyFriends.sortBy(function(friend) {
return friend.get("uid");
}));
Edit
Most of the Underscore methods in Backbone seem to be chainable (replace sortBy with reject, for example, and it runs). Looking at the Backbone source where they wire up the Underscore proxies, it seems that sortBy is treated differently. I can't understand why they do it this way ...
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
'isEmpty', 'chain'];
_.each(methods, function(method) {
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});
var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
_.each(attributeMethods, function(method) {
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};

Related

Filtering the collection doesn't work

I have a collection of models with boolean value interviewed and I want to filter them so that my view would display either models with this property set to true or models with this property set to false. In my collection I have the following methods:
var ResumeCollection = Backbone.Collection.extend({
filterActive: function () {
var active = this.where({interviewed: false});
return active;
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return interviewed;
}
});
and in my view I have the following:
var ResumeList = Backbone.View.extend({
events: {
'click #active': 'showActive',
'click #interviewed': 'showInterviewed'
},
initialize: function () {
this.collection = new ResumeCollection();
},
render: function () {
var self = this;
this.$el.html( $('#filter') );
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
},
showActive: function () {
this.collection.filterActive();
this.render();
},
showInterviewed: function () {
this.collection.filterInterviewed();
this.render();
}
});
But any time I click #active or #interviewed buttons, it happens nothing and the models with required properties aren't rendered. I've already tried to manage that with reset method or returning a new collection instance with required models, but that's not a solution, because when I succesfully filter the initial collection, it returns me a new collection with the models I need (e.g. models where interviewed: true), and I can't filter it more -- it returns just an empty collection.
I just can't get how can I filter this one collection in the way I need.
You're returning a successfully filtered collection, and then not doing anything with them.
showActive: function () {
this.collection.filterActive();//returns a value you're not using
this.render();
},
showInterviewed: function () {
this.collection.filterInterviewed();//returns a value you're not using
this.render();
}
I suggest adding an optional parameter to your render method that represents the filtered collection. If the parameter is defined, use it. If not, use the unfiltered collection.
Borrowing some of #Simo's code to return a new collection.
filterActive: function () {
var active = this.where({interviewed: false});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.where({interviewed: true});
return new ResumeCollection(interviewed);
},
render: function (filtered) {
var self = this;
var data = filtered ? filtered.toArray() : this.collection.toArray();
this.$el.html( $('#filter') );
_.each(data , function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
},
showActive: function () {
var filtered = this.collection.filterActive();
this.render(filtered);
},
showInterviewed: function () {
var filtered = this.collection.filterInterviewed();
this.render(filtered);
}
Your issue is that you are not returning the filtered collection.
This should work:
filterActive: function () {
var active = this.filter(function(item) {
return item.get('interviewed') === false;
});
return new ResumeCollection(active);
},
filterInterviewed: function () {
var interviewed = this.filter(function(item) {
return item.get('interviewed') === true;
});
return new ResumeCollection(interviewed);
},
i would suggest you to modify you render function to accept a argument which will be array of models.
Now when rendering full collection you can call render as
render(this.collection.models) // reference to list of models
also if you filter out the collection then the filter function most probably be returning the subset of models from collection. Which you can again pass to render function
this.render(this.showActive()) // showActive returns subset of models from collection
This way your render function becomes modular.. which accepts array and render then on page..
Now for Filtering out Collection you can use filter , where methods exposed by underscore .. Remember to capture the return and pass it along to render Function..

Backbone Collection View each

I'm new to Backbone.js and getting some trouble with collection view.
Here's what I'm trying to do:
var customersCollection = new _App.Collections.Customers();
var customersView = new _App.Views.Customers({collection: customersCollection});
customersView.render();
And here's a view - I can't understand why I cannot iterate over collection:
_App.Views.Customers = Backbone.View.extend({
render: function() {
console.log('Here is my collection');
console.log(this.collection);
console.log('Now lets iterate over it...');
_.each(this.collection, function(item) {
console.log(item);
}, this);
console.log('...done');
return this;
}
});
What I see in chrome console:
Here is my collection
child {length: 0, models: Array[0], _byId: Object, constructor: function, url: "/admin/customers/latest.json"…}
_byId: Object
length: 5
models: Array[5]
__proto__: Surrogate
Now lets iterate over it...
...done
So I can't figure out why I can see a collection but can't each over it.
Thanks
// SOLVED
I have found why this was going to happen.
Completely missed that .fetch() is asynchronous, so when render() was called, data were still not present in collection.
This code works for me now, so I can go on with templates, etc
_App.Views.Customers = Backbone.View.extend({
initialize: function() {
this.collection = new _App.Collections.Customers();
this.collection.on('sync', this.render, this);
this.collection.fetch();
},
render: function() {
this.collection.each(function(item) {
console.log(item);
});
return this;
}
});
new _App.Views.Customers();
Regards, Nikolay
You're not using _.each appropriately.
Should be:
_.each(this.collection.models, function(item) {
console.log(item);
},this);
or better yet:
this.collection.each(function(item) {
console.log(item);
});

Backbone - Possible to get the collection from a model

I'm wondering if there's a way to get a reference to a collection from one of its models. For instance, if any of the people in the collection below are somehow aware of belonging to a collection, or multiple collections. Fiddle
(function() {
window.App = {
Models: {},
Views: {},
Collections: {}
};
App.Models.Person = Backbone.Model.extend({
defaults: {
name: 'John',
phone: '555-555-5555'
}
});
App.Views.Person = Backbone.View.extend({
tagName: 'li',
template: _.template("<%= name %> -- <%= phone %>"),
render: function(){
var template = this.template( this.model.toJSON() );
this.$el.html( template );
return this;
}
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person
});
App.Views.People = Backbone.View.extend({
tagName: 'ul',
add: function(person){
var personView = new App.Views.Person({ model: person });
this.$el.append( personView.render().el );
return this;
},
render: function() {
this.collection.each(this.add, this);
return this;
}
});
})();
var peeps = [ { name: 'Mary' }, { name: 'David' }, { name: 'Tiffany' } ];
var people = new App.Collections.People(peeps);
var peopleView = new App.Views.People({ collection: people });
peopleView.render().$el.appendTo('body');
Each model has a property called collection. In your fiddle, adding console.log(people.models[0].collection) will print out the collection.
Looking through the source code, it looks like this is what's used to do things like remove a model from a collection when the model's destroy() method is called.
Update: see this updated fiddle which creates three person models and two collections. It prints them to the console. It looks like model.collection only refers to the first collection the person was added to, not the second.

Why are my Backbone Models nested strangely within a Collection, requiring drilling down to access methods/properties?

I've got a Collection and a Model, both using attributes/options to augment them with additional capabilities. Here's the Model (LoadRouteGroup):
return Backbone.Model.extend({
initialize: function () {
console.log(this);
},
fetchf: function () {
console.log("FETCH");
}
});
And the Collection (LoadRouteGroups):
return Backbone.Collection.extend({
constructUrl: function(options) {
if (options.groupingType === "facility") {
// TODO: new endpoint: /api/v1/loadroutes?grouping=facility
this.url = clawConfig.endpoints.webApiRootUrl + "/api/loads/facilities";
}
else {
this.url = clawConfig.endpoints.webApiRootUrl + "/api/v1/loadroutes";
}
},
initialize: function (models, options) {
options || (options = {});
this.constructUrl(options);
console.log(this);
}
});
They're instantiated as such:
var loadRouteGroup = new LoadRouteGroup({
entityType: "facility"
});
// WORKS
loadRouteGroup.fetchf();
// assign groupingType option to collection to denote which URL to use
var loadRouteGroups = new LoadRouteGroups({
model: loadRouteGroup
}, {
groupingType: "facility"
});
var firstGroup = loadRouteGroups.at(0);
// DOESN'T WORK
firstGroup.fetchf();
// WORKS
firstGroup.attributes.model.fetchf();
I would expect that call to firstGroup.fetchf() to work... but it doesn't. Instead, I have to weirdly drill down and use firstGroup.attributes.model.fetchf() in order to access the method.
What's going on here? This would seem straightforward to me, but I can't for the life of me figure out what's wrong with the relationship between my Collection and Model.
The collection definition should include the model type:
return Backbone.Collection.extend({
// ....
model: LoadRouteGroup
});
When initializing the collection, pass in an array of models:
var loadRouteGroup = new LoadRouteGroup({
entityType: "facility"
});
var loadRouteGroups = new LoadRouteGroups([loadRouteGroup], {
groupingType: "facility"
});
Specify the model when you extend the collection instead of when you instantiate.

Backbone model not instantiating in collection

I have a backbone model like so
define([
'underscore',
'backbone'
],function( _,Backbone) {
var Task = Backbone.Model.extend({
//api url
url:'',
methodToURL: {
'read': './api/tasks/index',
'create': './api/tasks/task',
'update': './api/tasks/task',
'delete': './api/tasks/task'
},
sync: function(method, model, options) {
options = options || {};
options.url = this.methodToURL[method.toLowerCase()];
Backbone.sync(method, model, options);
}
});
return Task;
});
And a collection
define(['underscore','backbone','models/task'],function( _,Backbone,Task) {
var TaskCollection = Backbone.Collection.extend({
//Model
model:Task,
//api url
url:'',
methodToURL: {
'read': './api/tasks/index',
'create': './api/tasks/task',
'update': './api/tasks/task',
'delete': './api/tasks/task'
},
sync: function(method, model, options) {
options = options || {};
options.url = this.methodToURL[method.toLowerCase()];
Backbone.sync(method, model, options);
},
//construct
initialize: function() {
this.sort_key = 'end';
this._model = new Task();
this.fetch();
},
comparator: function(a,b) {
a = a.get(this.sort_key);
b = b.get(this.sort_key);
return a > b ? 1
: a < b ? -1
: 0;
},
mark_complete: function(task_id) {
var task_status = 0;
console.log(this.model);
this.model.save({id:task_id,task_status:task_status});
},
mark_incomplete: function(task_id) {
var task_status = 1;
console.log(this.model);
this.model.save({id:task_id,task_status:task_status});
},
sort_by_status: function() {
this.sort_key = 'task_status';
this.sort();
},
sort_by_task_tag: function() {
this.sort_key = 'task_group';
this.sort();
}
});
return TaskCollection;
});
When i the mark_complete method runs the model is logged to the console, but it logs this
"function (){ parent.apply(this, arguments); } " and says "function (){ parent.apply(this, arguments); } has no method 'save'";
Am guessing the model is supposed to be instantiated so the collection can have access it to its methods, so what is wrong?
The model property is just a constructor that Collection uses when you add a model to the collection. It is intended to make your life easier when you try to input data to the collection. Instead of always calling the constructor when adding a Task model to TaskCollection, you'd just input a JavaScript object and it will do the same thing.
So this is how your code would look like when you would want to insert a model without setting the model property to your TaskCollection
taskCollection.add(new Task({
name: "Get Milk",
description: "We're out of milk. There's a sale going on at the local supermarket."
}));
// If you wanted to just input just the JSON object without calling the
// constructor, then you can't.
And this is how your code would look like if you had set the model property
taskCollection.add({
name: "Get Milk",
description: "We're out of milk. There's a sale going on at the local supermarket."
});
As you can see, you don't need to call the Task constructor; the instance of TaskCollection will call it for you.
And this is why instances of TaskCollection will only have the model property set to the actual constructor of Task, not an initialized version.

Categories

Resources