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.
Related
I have a few models that don't just contain basic data attributes, but they might have one or two attributes that hold another models object.
This has been okay, but now I want to call
myRootModel.toJSON()
and I've noticed that it doesn't call .toJSON on the other models in my model that I'm trying to call toJSON() on.
Is there a way to override backbone model .toJSON to go through all fields, recursively, whether they are basic attributes, sub-models or collections? If not, can I override toJSON in each model / collection?
I'm aware of backbone-relational, but I don't want to go that route - I'm not using fetch/save, instead our API returns responses that I adjust in the models parse function and simply invoke new MyRootModel(data,{parse:true}).
Here's a way you can achieve such a thing (there's maybe another way):
Backbone.Model.prototype.toJSON = function() {
var json = _.clone(this.attributes);
for(var attr in json) {
if((json[attr] instanceof Backbone.Model) || (json[attr] instanceof Backbone.Collection)) {
json[attr] = json[attr].toJSON();
}
}
return json;
};
http://jsfiddle.net/2Asjc/.
Calling JSON.parse(JSON.stringify(model)) parses the model with all the sub-models and sub-collections recursively. Tried on Backbone version 1.2.3.
I read several articles about Backbone.js with sample apps but I can't find an explanation or example on how Backbone knows when a widget in a view is clicked and to which model it is bound.
Is it handled by internal assignment of IDs or something?
For example if you want to delete a div with id="123" could remove it from the DOM with jQuery or javascript functions. In backbone this div could be without the id but could be removed without knowing it, right?
If anybody knows a good article or could improve my understanding on that it would be great.
The way the view "knows" the model to which it's bound is done through the _configure method shown below:
_configure: function(options) {
if (this.options) options = _.extend({}, this.options, options);
for (var i = 0, l = viewOptions.length; i < l; i++) {
var attr = viewOptions[i];
if (options[attr]) this[attr] = options[attr];
}
this.options = options;
}
The import block to note is:
for (var i = 0, l = viewOptions.length; i < l; i++) {
var attr = viewOptions[i];
if (options[attr]) this[attr] = options[attr];
}
viewOptions is an array of keys that have "special" meaning to a view. Here's the array:
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
This loop is the "glue" between view and model or view and collection. If they're present in the options, they're assigned automatically.
All this is in the annotated source code.
Check http://www.joezimjs.com/javascript/introduction-to-backbone-js-part-1-models-video-tutorial/.
Even if it looks complicated, there's so little to learn, trust me.
If you ask more specifically I could try to help.
Reading the source is probably your best bet for improving your understanding. The Backbone function you want to look at is called delegateEvents. But the short version is that it uses the jQuery delegate() function. The root element is the View's element (the el property), and it's filtered by whatever selector you provided.
jQuery doesn't actually bind a handler to each element that you're listening to. Instead it lets the events bubble up to the root element and inspects them there. Since there's nothing attached to each individual element you can delete them freely without causing any problems. However some methods of deleting the View's element (eg, by setting innerHTML on a parent element) might cause a memory leak. I'm not 100% sure about that, but it's probably best to just not do that anyway.
On the first attempt I wrote
this.collection.each(function(element){
element.destroy();
});
This does not work, because it's similar to ConcurrentModificationException in Java where every other elements are removed.
I tried binding "remove" event at the model to destroy itself as suggested Destroying a Backbone Model in a Collection in one step?, but this will fire 2 delete requests if I call destroy on a model that belongs to a collection.
I looked at underscore doc and can't see a each() variant that loops backwards, which would solve the removing every element problem.
What would you suggest as the cleanest way to destroy a collection of models?
Thanks
You could also use a good, ol'-fashioned pop destroy-in-place:
var model;
while (model = this.collection.first()) {
model.destroy();
}
I recently ran into this problem as well. It looks like you resolved it, but I think a more detailed explanation might also be useful for others that are wondering exactly why this is occurring.
So what's really happening?
Suppose we have a collection (library) of models (books).
For example:
console.log(library.models); // [object, object, object, object]
Now, lets go through and delete all the books using your initial approach:
library.each(function(model) {
model.destroy();
});
each is an underscore method that's mixed into the Backbone collection. It uses the collections reference to its models (library.models) as a default argument for these various underscore collection methods. Okay, sure. That sounds reasonable.
Now, when model calls destroy, it triggers a "destroy" event on the collection as well, which will then remove its reference to the model. Inside remove, you'll notice this:
this.models.splice(index, 1);
If you're not familiar with splice, see the doc. If you are, you can might see why this is problematic.
Just to demonstrate:
var list = [1,2];
list.splice(0,1); // list is now [2]
This will then cause the each loop to skip elements because the its reference to the model objects via models is being modified dynamically!
Now, if you're using JavaScript < 1.6 then you may run into this error:
Uncaught TypeError: Cannot call method 'destroy' of undefined
This is because in the underscore implementation of each, it falls back on its own implementation if the native forEach is missing. It complains if you delete an element mid-iteration because it still tries to access non-existent elements.
If the native forEach did exist, then it would be used instead and you would not get an error at all!
Why? According to the doc:
If existing elements of the array are changed, or deleted, their value as passed to callback will be the value at the time forEach visits them; elements that are deleted are not visited.
So what's the solution?
Don't use collection.each if you're deleting models from the collection. Use a method that will allow you to work on a new array containing the references to the models. One way is to use the underscore clone method.
_.each(_.clone(collection.models), function(model) {
model.destroy();
});
I'm a bit late here, but I think this is a pretty succinct solution, too:
_.invoke(this.collection.toArray(), 'destroy');
Piggybacking on Sean Anderson answer.
There is a direct access to backbone collection array, so you could do it like this.
_.invoke(this.collection.models, 'destroy');
Or just call reset() on the collection with no parameters, destroy metod on the models in that collection will bi triggered.
this.collection.reset();
http://backbonejs.org/#Collection-models
This works, kind of surprised that I can't use underscore for this.
for (var i = this.collection.length - 1; i >= 0; i--)
this.collection.at(i).destroy();
I prefer this method, especially if you need to call destroy on each model, clear the collection, and not call the DELETE to the server. Removing the id or whatever idAttribute is set to is what allows that.
var myCollection = new Backbone.Collection();
var models = myCollection.remove(myCollection.models);
_.each(models, function(model) {
model.set('id', null); // hack to ensure no DELETE is sent to server
model.destroy();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone-min.js"></script>
You don't need underscore and for loop for this.
this.collection.slice().forEach(element => element.destroy());
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 have a basic collection :
myCollection = Backbone.Collection.extend({
model: myModel
url: '/theurl',
initialize: function(){
this.fetch();
})
})
When initialized, the collection receives items ordered by date.
I would like to be able to dynamically reorder the collection, using another model attribute (name, rating, etc.).
In the view associated with the collection, I tried to bind a click event to a callback like this :
reorderByName: function(){
myCollection.comparator = function(item){
return item.get('name');
});
this.render();
})
Unfortunately, that does not seem to work. Any suggestions as to how I could do it ?
Thanks.
It looks like you've only done half of what you need to do. You've given the collection its comparator, but you've not told it to resort itself. So you need to add something like this statement right before you render:
myCollection.sort();
Note that this will trigger a reset event which would be received by any object that you've bound this event to. If you wish to suppress triggering that event, you could make the call this way:
myCollection.sort({silent: true});
Hope this helps.
I found this while looking for a solution to my own issues with comparator. I thought I would add, in case anyone else finds this question, that if the models in your collections have undefined values for the property by which your comparator is sorting, the sort won't happen. Be sure to use defaults on your model if they're not required and you're gonna sort by 'em. ;)