I am trying to find a way to efficiently display a sorted Marionette.CollectionView, sorted by multiple attributes without modifying the underlying Backbone collection. For this example, I am using 'name' and 'online' attributes, and want my CollectionView to be displayed in 2 parts:
online, alphabetically
offline, alphabetically
I have a single Collection of Backbone.Models and want to use this across my web application. So having a sort function on my Collection doesn't feel right to me.
My example code is as follows:
var MyCollection = Backbone.Collection.extend({
model:Backbone.Model
});
var myCollection = new MyCollection();
myCollection.set([
{ name : 'Freddy', online : true },
{ name : 'Zorro', online : false },
{ name : 'Charlie', online : false },
{ name : 'Alice', online : true }
]);
var MyView = ...
/*
omitted for brevity, though my template is like
<li>{{name}} {{#if online}}(Online){{/if}}</li>
*/
var MyCollectionView = Marionette.Collection.extend({
childView:MyView,
viewComparator: function (item1, item2) {
var item1Online = item1.get('online');
var item2Online = item2.get('online');
if (item1Online != item2Online)
return item1Online ? -1 : 1;
var item1Name = item1.get('name');
var item2Name = item2.get('name');
return item1Name.localeCompare(item2Name);
}
});
var myCollectionView = new MyCollectionView({collection:myCollection});
appView.getRegion('example').show(myCollectionView);
I would like this to be displayed as:
Alice (Online)
Freddy (Online)
Charlie
Zorro
This is fine when all of the data is added to the collection at once, or add/remove events but if one of the attributes is updated on a model that is already in the collection, the view does not update.
If Charlie's 'online' property changed to true - e.g by performing.
charlieModel.set('online', true)
I would like the CollectionView to have rendered automatically as:
Alice (Online)
Charlie (Online)
Freddy (Online)
Zorro
Any suggestions? Many thanks in advance.
From the backbone documentation
Collections with a comparator will not automatically re-sort if you later change model attributes, so you may wish to call sort after changing model attributes that would affect the order.
You can put somewhere convenient in your code a listener on a change in the model attributes your are targeting that will trigger a re-sort of your collection.
// for example in your collection
initialize: function() {
this.on('change:name', function() { this.sort() }, this);
}
The Marionette team advised having a separate Backbone collection behind the scenes, rather than using the viewComparator on the CollectionView.
Using reorderOnSort on the CollectionView made a huge difference (at least 10x speed up) in terms of render speed.
This option is useful when you have performance issues when you resort your CollectionView.
Without this option, your CollectionView will be completely re-rendered, which can be
costly if you have a large number of elements or if your ChildViews are complex. If this option
is activated, when you sort your Collection, there will be no re-rendering, only the DOM nodes
will be reordered.
My final example code:
var MyView = Backbone.Marionette.ItemView.extend({
modelEvents:{
'change:name change:online': 'render'
},
template:template
});
var MyCollection = Backbone.Collection.extend({
initialize : function(){
this.on('change:name change:online', this.sort, this);
},
comparator : function(item1, item2){
var item1online = item1.get('online');
var item2online = item2.get('online');
if (item1online != item2online)
return item1online ? -1 : 1;
return item1.get('name').localeCompare(item2.get('name'));
}
});
var myCollection = new MyCollection();
var MyCollectionView = Marionette.CollectionView.extend({
childView : MyView,
reorderOnSort : true
});
var myCollectionView = new MyCollectionView({
collection : myCollection
});
appView.region('example').show(myCollectionView);
Related
In my backbone.marionette app, I am trying to set a model attribute 'rank' as it's loop index. here is the code of my collection:
AngryCats = Backbone.Collection.extend({
model:AngryCat,
initialize: function(cats){
var rank = 1;
_.each(cats, function(cat){
cat.set('rank', rank);
rank++;
})
}
});
But I am getting error as :
TypeError: cat.set is not a function
cat.set('rank', rank);
any one tell me what is wrong here? ( please check the fiddle link for complete coding )
Live Demo
You are passing array of javascript objects. But the set is only available in Backbone.Model instance. Only after initialisation, every single object converted into Backbone.Model Object.
You have to do like
var rank = 1;
_.each(cats, function(cat){
cat.rank = rank;
rank++;
})
How does one create a collection (in backbone.js) that is organized by an id. For example I would think it's quite common that collections need to be organized by date, category, keyword, type the list goes on. In my case I am trying to find an elegant way to organize players to team.
I don't think I need a plugin for this, my goal is just to organize the players to be grouped inside a div while using one clean json file so I can have a nice organized list. Again ideally it would be nice to use one json file for this, and structure the HTML similar to how the json itself is structured in terms of nesting, in my example league being the parent of everything, then team being the parent of the players.
I have gone through many backbone tutorials multiple times, and understand the flow and syntax pretty comfortably, but all tutorials work with one collection outputting a simple list in no specific order.
Basically I want to be able to create a clean json array like below. Then turn it into nice organized HTML.
{
"league":
[{
"team":"Lakers",
"players": [
{
"name": "Kobe"
},
{
"name": "Steve"
}
]
},
{
"team":"Heat",
"players": [
{
"name": "LeBron"
},
{
"name": "Mario"
}
]
}]
}
So this json structure is valid but it's not one I have used it's nested a little bit more, so accessing the models requires a little different technique, I am hoping to learn if this kind of array is ideal? Also if so how would I group say Kobe, Steve inside a div with perhaps the class name Lakers whilst obviously separating it from LeBron and Mario keeping those two inside a div with again perhaps a class of Heat.
Another example I could use would be like a actors to movies collection, again is the json format ideal (seems like it to me) here I obviously group actors to their respected movie? I would greatly appreciate some tips on building a clean view or views with a template or templates for outputting this nice and tidy.
{
"movies":
[{
"title":"Prisoners",
"actors": [
{
"name": "Hugh Jackman"
},
{
"name": "Jake Gyllenhaal"
}
]
},
{
"title":"Elysium",
"actors": [
{
"name": "Matt Damon"
},
{
"name": "Jodie Foster"
}
]
}]
}
Again just working with this json data and backbone.js how do I create a maintainable view for this array?
Final Note: I was able to successfully group players to teams using two json files, and assigning a player and id that matched the team id, then looped the team collection with the player collection inside using where to organize it (I am trying to rethink this better). To me this is not taking advantage of backbone, it can get confusing and just seems wrong to me. So again I hope to improve my knowledge here and get better. I would immensely appreciate clear concise information, I really struggle to wrap my head around this topic :)
THANKS!!
Keep your JSON in a Backbone friendly structure, this will mean your models are easily organised once they are placed into the collection.
JSON example
[
{ league : 1, team : 'lakers', player : 'kobe' }
{ league : 1, team : 'lakers', player : 'steve' }
// And so on
]
Consider that most backbone collections are built via a RESTful JSON api this would be easy to fetch directly into a collection and then sorted. The comparator() function on the collection is run each time a model is added to the collection, or when you ask for it to run.
Backbone collection example
var Players = Backbone.Collection.extend({
initialize : function() {
// Grab the JSON from the API
// this.fetching is now a deferred object
this.fetching = this.fetch();
}
// Comparator example one, as a string
comparator : 'team',
// Comparator example two, as a function
comparator : function(model) {
return model.get('team');
}
});
The comparator as a function approach is obviously better suited to more complex sort algorithms, otherwise the comparator as a string approach would be better. Bear in mind that the function approach, though a string can be returned (such as above), -1 or 1 would be better return values to indicate it's sort position.
Comparator example with model comparison
comparator : function(modelA, modelB) {
var teamA = modelA.get('team'),
playerA = modelA.get('player'),
teamB = modelB.get('team'),
playerB = modelB.get('player');
if (teamA < teamB) { // string comparison
return 1;
} else if (playerA < playerB} {
return 1;
} else {
return -1;
}
}
Now whenever a model is added to the collection it is sorted into it's correct location, if using the last example, by team and then by player name.
Simple view example using the collection
var ViewExample = Backbone.View.extend({
el : "#example-container",
render : function() {
var self = this;
// Use the deffered object to make sure models are
// all available in the collection before we render
this.collection.fetching.done(function() {
self.collection.each(function(model) {
self.$el.append('<p>' + model.get('player') + '</p>');
});
});
return this;
}
});
// Create the view and pass in the collection
// that will immediately fetch it's models
var view = new ViewExample({
collection : new Players()
});
Code here is untested
Start by building a working model of your data. Your JSON suggests a hierarchy : a collection of teams that each have a collection of players. Here's a possible implementation :
var Player = Backbone.Model.extend();
var Players = Backbone.Collection.extend({
model: Player
});
var Team = Backbone.Model.extend({
constructor: function(data, opts) {
// I like my subcollections as attributes of the model
// and not on the settable properties
this.players = new Players();
Backbone.Model.call(this, data, _.extend(opts, {parse: true}));
},
parse: function(data) {
// Players are handled in a subcollection
if (_.isArray(data.players))
this.players.reset(data.players);
// They are removed from the model properties
return _.omit(data, 'players');
}
});
var Teams = Backbone.Collection.extend({
model: Team,
parse: function(resp) {
return resp.league;
}
});
Now you can create your root collection. Note that you could also fetch it instead of instantiating it with the data.
// teams list
var teams = new Teams(data, {parse: true});
// for example, to get all players in all teams
var allplayers = _.flatten(teams.map(function(team) {
return team.players.models;
}));
console.log(_.invoke(allplayers, 'get', 'name'));
And a demo : http://jsfiddle.net/8VpFs/
Once you have your structure in place, you can worry about rendering it. Let's imagine you have this (Underscore) template
<ul>
<% _(teams).each(function(team) { %>
<li><strong><%= team.team %></strong>
<ul>
<% _(team.players).each(function(player) { %>
<li><%= player.name %></li>
<% }); %>
</ul>
</li>
<% }); %>
</ul>
You can alter your Team model to output a serialized representation of you model:
var Team = Backbone.Model.extend({
// ... as before
toJSON: function() {
var json = Backbone.Model.prototype.toJSON.call(this);
json.players = this.players.toJSON();
return json;
}
});
and render your template
var tpl = _.template($('#tpl').html());
var html = tpl({
teams: teams.toJSON()
})
$('body').append(html);
This usually would go into a view.
http://jsfiddle.net/8VpFs/1/
With a fetch
var teams = new Teams();
teams.fetch().then(function() {
var tpl = _.template($('#tpl').html());
var html = tpl({
teams: teams.toJSON()
});
$('body').append(html);
});
http://jsfiddle.net/8VpFs/2/
I have an array whose items I want to group, and then display in this grouped fashion. It's all terribly confusing:
App.GroupedThings = Ember.ArrayProxy.extend({
init: function(modelToStartWith) {
this.set('content', Ember.A());
this.itemsByGroup = {};
modelToStartWith.addArrayObserver(this, {
willChange: function(array, offset, removeCount, addCount) {},
didChange: function(array, offset, removeCount, addCount) {
if (addCount > 0)
// Sort the newly added items into groups
this.add(array.slice(offset, offset + addCount))
}
});
},
add : function(things) {
var this$ = this;
// Group all passed things by day
things.forEach(function(thing) {
var groupKey = thing.get('date').clone().hours(0).minutes(0).seconds(0);
// Create data structure for new groups
if (!this$.itemsByGroup[groupKey]) {
var newArray = Ember.A();
this$.itemsByGroup[groupKey] = newArray;
this$.get('content').pushObject({'date': groupKey, 'items': newArray});
}
// Add to correct group
this$.itemsByGroup[groupKey].pushObject(thing);
});
}
});
App.ThingsRoute = Ember.Route.extend({
model: function() {
return new App.GroupedThings(this.store.find('thing'));
},
});
This only works if I use the following template:
{{#each model.content }}
These don't render anything (an ArrayController is used):
{{#each model }}
{{#each content }}
{{#each}}
Why? Shouldn't the ArrayController proxy to "model" (which is GroupedThings), which should proxy to "content"?
The reason this becomes a problem is that I then want to sort these groups, and as soon as I change the entire contents array (even using ArrayProxy.replaceContent()), the whole views rebuilt, even if only a single item is added to a single group.
Is there a different approach to this entirely?
I've tended to use ArrayProxies slightly differently when doing such things.
I'd probably get Ember to do all the heavy lifting, and for sorting get it to create ArrayProxies based around a content collection, that way you can sort them automatically:
(note I haven't run this code, but it should push you off in the right direction)
App.GroupedThings = Em.ArrayProxy.extend({
groupKey: null,
sortKey: null,
groupedContent: function() {
var content = this.get('content');
var groupKey = this.get('groupKey');
var sortKey = this.get('sortKey');
var groupedArrayProxies = content.reduce(function(previousValue, item) {
// previousValue is the reduced value - ie the 'memo' or 'sum' if you will
var itemGroupKeyValue = item.get('groupKey');
currentArrayProxyForGroupKeyValue = previousValue.get(itemGroupKeyValue);
// if there is no Array Proxy set up for this item's groupKey value, create one
if(Em.isEmpty(currentArrayProxyForGroupKeyValue)) {
var newArrayProxy = Em.ArrayProxy.createWithMixins(Em.SortableMixin, {sortProperties: [sortKey], content: Em.A()});
previousValue.set(itemGroupKeyValue, newArrayProxy);
currentArrayProxyForGroupKeyValue = newArrayProxy;
}
currentArrayProxyForGroupKeyValue.get('content').addObject(item);
return previousValue;
}, Em.Object.create());
return groupedArrayProxies;
}.property('content', 'groupKey', 'sortKey')
);
You'd then Create a GroupedThings instance like this:
var aGroupedThings = App.GroupedThings.create({content: someArrayOfItemsThatYouWantGroupedThatHaveBothSortKeyAndGroupKeyAttributes, sortKey: 'someSortKey', groupKey: 'someGroupKey'});
If you wanted to get the groupedContent, you'd just get your instance and get.('groupedContent'). Easy! :)
...and it'll just stay grouped and sorted (the power of computed properties)... if you want an 'add' convenience method you could add one to the Em.Object.Extend def above, but you can just as easily use the native ones in Em.ArrayProxy, which are better IMHO:
aGroupedThings.addObjects([some, set, of, objects]);
or
aGroupedThings.addObject(aSingleObject);
H2H
I want to create a table that is sortable using the attributes of a collection.
So far i've being able to make the tab table sortable using two attributes, but i would like to be sortable based the value of the sort key attribute.
e.g when the "task_status = 'open'"
Here what i having working now
var TaskCollection = Backbone.Collection.extend({
//Model
model:Task,
//url
url:"./api/tasks",
//construct
initialize: function() {
this.sort_key = 'end';
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;
},
sort_by_status: function() {
this.sort_key = 'task_status';
this.sort();
},
sort_by_task_tag: function() {
this.sort_key = 'task_group';
this.sort();
}
});
This sorts the collection but does not reverse the order, or allow me to sort by the particular value of an attribute. How can this be modified to work
In the comparator, have a state variable for "reversed" and have it take values 1 and negative 1. Multiply it by the previous return value. Setting the state variable on the collection and then again sorting should get things done.
I am building a web app using backbone.js as an MVC framework. I am struggling in designing my models. I have one parent/main object but it is quite complex/nested in nature i.e over 50 attributes and some nesting going on. What i am struggling with is:
I have something like:
{section1:{
key1:"value1",
key1:"value2"},
section2:{
key1:"value1",
key1:"value2"}
}
Should i flatten the object out like:
{section1_key1:"value1",
section1_key2:"value2",
section2_key1:"value1",
section2_key2:"value2"
}
or should I:
use the backbone-nested plug-in and pass in the large nested JSON object as is?
or create separate models for each section and nest somehow within the parent
model.
or simply create models for the child objects and not worry about the nesting and add some type of reference
Suggestions appreciated.
I'm struggling on this right now as well.
I'm leaning towards option 2.
I have each of the nested sections built out into separate models, and creating an interface similar to SQLObject in Python (or really any ORM)
So given a simple blog post idea:
var Tag = Backbone.Model.extend({
defaults: {
name: ''
},
validate: function(atts){
if(!atts.name || atts.name.length < 2){
return "You must provide a name that's longer than 2 characters";
}
}
});
var TagList = Backbone.Collection.extend({
model: Tag
})
var Entry = Backbone.Model.extend({
defaults: {
author: "Todd",
pub_date: new Date(),
content: "",
title: "",
tags: new TagList()
},
add_tag: function(tag){
var tag_collection = this.get('tags');
tag_collection.add({name: tag});
this.set({tags: tag_collection});
},
remove_tag: function(tag){
tag.destroy();
},
tags: function(){
return this.get('tags').models;
}
});
An alternate idea would to let initialize handle the de-nesting and creation and adding of items to the collection that is then stored as a property on that model.