In a collection, how can we change the order of models?
This is not about sorting the models, or writing a comparator function to achieve a sorting order.
When a collection is built from an array of objects, the order of models will be the insertion order.
Now my question is: After the collection is built, how to change the position of the models as per my preference.
For example,
When the collection is built, order of models as per the original array like: 0, 1, 2, 3, 4, 5.
Now, I want to change the order like 2, 4, 0, 1, 3, 5.
In fact it boils down to writing a comparator -- if you want to impose a certain order of the models in the collections, you are in fact sorting them. To achieve what you want it seems easiest to add some order attribute to the model and then write a comparator working on it.
I know this is an old post, but I had the same one and came up with a solution. In short, my solution is to replace the collection.models list with a new list containing the same models but in the order I wanted it to be in... Below is my use case and example (using coffee script).
NOTE: I am not 100% sure this will not be buggy since this changes collection's list rather than sorting the original list in-place. But I have not yet found a failure mode and I've tested several.
My use case was a DOM list that is sortable (via jquery.ui/sortable). I need the collection's order to reflect what is shown on the DOM.
Each sortable DOM element has a data-cid attribute corresponding to the model, I did this in the initialize method of each Backbone.View list item.
class ListItem extends Backbone.View
...
initialize: (options = {}) ->
#$el.attr "data-cid", #model.cid
The parent Backbone.View (an unordered list) listens to a sortupdate event (the user sorted something), then constructs a list of cids with the new order, and passes this into the collection to be sorted.
class List extends Backbone.View
...
tagName: "ul"
events:
"sortupdate" : "sort"
render: ->
#$el.html("") # reset html
#collection.each (model) =>
view = new ListItem model: model
#$el.append view.render().$el
return #
sort: (e, ui) ->
e.stopPropagation()
orderByCid = #$el.children().map(-> $(#).attr "data-cid")
#collection.orderModels orderByCid
The collection makes a new list of models in the same order as the cids using Backbone.Collection::get and then replaces its model list with this.
class DataList extends Backbone.Collection
...
orderModels: (cids = []) ->
# Add validation that all the cids exist in the collection
# and that list and collection are the same length!
result = []
result.push #get(cid) for cid in cids
#models = result
Related
I have a collection A and a view A
collection A:
model {type: A}
model {type: B}
view A use collection A and all its models.
If I add a new model to collection A, this model is added to view A.
In view B I want to use collection A as well, but not all its models, only models with type B. But I want to use all listeners in View A.
So, If I add a new model with type B in view A, listeners in view B should intercept this and add it to view B.
I can make two different collections, and use two listeners. But this looks dirty.
Is there a way to get get a selection of a collection into a new collection, but keep the same listeners and backbone functions as if it was just a reference to the main collection?
One solution would be to have one collection replicate another (applying a filter at the same time).
From https://jsfiddle.net/t8e6Ldue/
var collectionB = new FilteredCollection(null, {
source: collectionA,
filter: function(model) {
return model.get('type') === 'B';
}
});
see the JSFiddle for full code
I have 2 post collections and a model as follows.
# router file
#posts = new MyApp.Collections.PostsCollection()
#posts.reset options.posts
#followed_posts = new MyApp.Collections.PostsCollection()
#followed_posts.reset options.followed_posts
# Post model file
class MyApp.Models.Post extends Backbone.Model
paramRoot: 'post'
follow_post: ->
# ajax call
console.log "_________Index:#{this.collection.indexOf(this);}"
console.log this.collection
console.log "_________Followed:"
console.log #followed_posts
class MyApp.Collections.PostsCollection extends Backbone.Collection
model: MyApp.Models.Post
url: '/posts_all'
What I am trying to do is when one of the model changed in one collection, I want to update the other model in other collection too.
These collections may or may not hold same models.
So let's say if a model in #posts changed in my Post model, I want to update that model in #followed_posts too. If #followed_posts doesn't have that model, I need to add a duplicate of the model to #followed_posts collection.
I can access the collection that model belongs, but I cannot access the other collection.
Any ideas appreciated, thanks.
If the two collections are antisocial and can't talk directly to each other, which is usually good design, then you'll need an intermediary -- a global event dispatcher. When a model changes, propagate that event to the dispatcher along with a reference to the model. Listen for the event in the other collection and use the model passed to check for existence and respond as needed.
EDIT:
Backbone's documentation mentions this pattern:
For example, to make a handy event dispatcher that can coordinate
events among different areas of your application: var dispatcher =
_.clone(Backbone.Events)
But in fact, this is such a common pattern that the Backbone object itself is extended with Events. So you can just do:
// In your Post model
#on "change", -> Backbone.trigger "post:change", this, #collection
// And then something like this in the collection class definition:
#listenTo Backbone, "post:change", (model, collection) =>
if post = #get model.cid
post.set model.toJSON()
else
#add model
Also, is followed posts a subset of posts? If so, why not put an attribute on the model designating it as followed? Then you could find all followed posts with a simple filter function.
I would strongly suggest that you should consider having a single collection and add some kind of attribute in the model to differentiate between what kind of posts they are.
Please consider following scenario:
<ParentView>
<FilterSubview></FilterSubview>
<ListSubview></ListSubview>
</ParentView>
To give you and example: I have a view which in turn shows view with filter (user can select to display books, magazines or both of them) and the list with items.
Both filter and list have corresponding models. Filter - what can we filter. List - list of all items.
Use case: user sees the full list and then can filter results by selecting only desired category.
Questions:
How those two views should interact? Should they know about each other or should parent view handle it?
Who should store filtered list to display? It could be list subview model directly or parent view can filter complete list and then pass it to render.
There is no one correct answer to your questions, but I'll try to explain a common, idiomatic way here.
Two sibling views should not know of each other. Instead they should interact via events through some kind of a mediator. Since in your case both FilterView and ListSubView share a common parent view which is responsible for rendering both of them, you could let the parent view mediate the events:
var ParentView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.filterView, "filter", this.filterChanged);
},
filterChanged: function(filterValue) {
this.listSubView.filter(filterValue);
}
});
var FilterView = Backbone.View.extend({
events: {
"change .filter" : "filterValueChanged"
},
filterValueChanged: function() {
var filterValue = //get filter value...
this.trigger("filter", filterValue);
}
});
Alternatively (preferrably, even) you can cut out a middle man and use the Mediator pattern. For that you need a third component whose job it is to pass messages between parties who should not know of each other. If you're using Backbone 0.9.9, there's just such a mediator built in: the Backbone root object works as a global event bus for this purpose.
So:
//ListSubView
this.listenTo(Backbone, "listfilterchanged", this.filterChanged);
//FilterView
Backbone.trigger("listfilterchanged", filterValue);
Then there's the question of who should be responsible of the list data. I tend to prefer to have the most specialized component be in charge, but so that only one component is in charge. In your case that would mean that the ListSubView should manage the filtered list, but only if the ParentView doesn't need to operate on it. That's just a generalization though, so take it with a grain of salt and do what feels right for your case.
In my app I have a socket.io connection that is listening to the backend and getting updates to models held by the clients browser (which retrieves the model by id and calls set on the model attribute).
I'd like the collection to be sorted, then re-rendered as a whole in order to reflect any new ordering on the models as a result of the set (most examples seem to be around individual views being re-rendered). What's a method of achieving this?
NB
I've got a backbone.js layout lifted pretty verbatim from the example todo app (this is the first backbone app).
You can achieve what you want by providing a comparator method for your collection.
Example:
ModelCollection = Backbone.Collection.extend({
comparator: function(a, b) {
if ( a.get("name") > b.get("name") ) return 1;
if ( a.get("name") < b.get("name") ) return -1;
if ( a.get("name") === b.get("name") ) return 0;
},
initialize: function() {
this.on('change:name', function() { this.sort() }, this);
}
});
The comparator in this example will cause your collection to be sorted in ascending order by the name attribute of the models inside.
Note that your collection won't be sorted automatically when changing attribute(s) of any of its models. By default, sorting happens only when creating new models and adding them to the collection; but the comparator will be used by the collection.sort method.
The code above takes advantage of this by setting an event listener that simply re-sorts the collection on any changes to the name attributes of its models.
To complete the picture, we set up an appropriate event listener in the View associated with the collection to make sure it re-renders on any changes:
CollectionView = Backbone.View.extend({
initialize: function() {
this.collection = new ModelCollection();
this.collection.on('all', function() { this.render() }, this);
},
render: function() {
this.$el.html(this.collection.toJSON());
}
});
That's it :)
Relevant excerpt from the Backbone documentation:
By default there is no comparator for a collection. 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. [...] 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.
I'm using a Signalr hub to subscribe to events on the server. What an event is dispatched to a hub, its successfully adding the item to a Marionette CollectionView. This, in turn, is rendered to a table.
Because the table of events is essentially a blotter, I'd like the events in reverse order and preferably only keep n-number of events.
Can Backbone 'automatically' re-render a collection in reverse order?
To go through collection in the reverse order I usually use a construction like this:
_.each(collection.last(collection.length).reverse(), function(model){ });
There is a thread on this topic at https://github.com/marionettejs/backbone.marionette/issues/78
Although Backbone keeps the collection sorted once you define a comparator, as #breischl pointed out, Marionette does not automatically re-render the CollectionView when order changes. In fact, Marionette listens to the add event on the collection and appends a new ItemView.
If you want your CollectionView to always display items in reverse chronological order, and you want new items added to be prepended instead of appended, then override the appendHtml method in your CollectionView as follows:
var MyCollectionView = Backbone.Marionette.CollectionView.extend({
appendHtml: function(collectionView, itemView){
collectionView.$el.prepend(itemView.el);
}
});
If you want to be able to insert at a particular location as #dira mentioned in the comment, there is a solution posted at the link above on github by sberryman that I reproduce here for convenience (disclaimer: I haven't tested the code below personally):
Change appendHtml to:
appendHtml: function(collectionView, itemView) {
var itemIndex;
itemIndex = collectionView.collection.indexOf(itemView.model);
return collectionView.$el.insertAt(itemIndex, itemView.$el);
}
And add the following to extend jQuery to provide insertAt function:
(function($) {
return jQuery.fn.insertAt = function(index, element) {
var lastIndex;
if (index <= 0) return this.prepend(element);
lastIndex = this.children().size();
if (index >= lastIndex) return this.append(element);
return $(this.children()[index - 1]).after(element);
};
})(jQuery);
Usually you'll have the rendering take place in your Backbone.View 'subclass'. So you have something like:
render: function() {
this.collection.each( function(model) {
// some rendering of each element
}, this );
}
this.collection is presumably a Backbone.Collection subclass, and so you can just use underscore.js methods on it to get it in whatever order you like:
this.collection.reverse().each( ... )
this.collection.sort( function(m) { ... } ).each( ... )
Etc.
Of course, you are getting a single element from your backend, and you want to insert it in the right place without re-rendering the whole thing! So in that case just go old school and insert your sort key as a rel attribute or data attribute on the elements, and use that to insertAfter or similar with jQuery in your renderNewItem (or similar) method.
Backbone automatically keeps Collections in sorted order. If you want to use a non-default sort, define a comparator() function on your Collection and it will use that instead. The comparator can take either one or two arguments, see the Backbone documentation for details.
You can then render your collection in an .each() loop, and it will come out in the correct order. Adding new items to the view in sorted order is up to you, though.
From what you describe, you don't need to re-render the collection in reverse order. Just add an event for add on your collection in that view and have it call a function that renders the item just added and prepends it to the table.
this.collection.on('add', this.addItem);
You can reverse your models in a collection like so...
this.collection.models = this.collection.models.reverse()
If you use lodash instead of underscore you can also do this:
_(view.collection.models).reverse();
As the BackBone doesn't support reverse iteration of the collection (and it's just waste of resources to reverse or worse sort the collection) the easiest and fastest approach is to use the for loop with decreasing index over models in the collection.
for (var i = collection.length - 1; i >= 0; i--) {
var model = collection.models[i];
// your logic
}
It's not that elegant as sorting or reversing the collection using Underscore but the performace is much better. Try to compare different loops here just to know what costs you to write foreach instead of classic for.