Backbone.js behind the scenes - backbone.js

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.

Related

$scope and counting children of an element in AngularJS

I'm pretty new to AngularJS, and I have a jQuery background so that could influence my way of thinking the problem.
In order to do DOM manipulation through transclude directive (i.e. adding a specific class) I need to know how many children (or maybe siblings) has a generic element.
What I mean is I would like to set a class on all children, based on an algorithm that counts the number of children themselves.
This is what I tried so far
var main = angular.module("Main",[]);
function utilities(){
this.consoleScope = function($scope){
return $scope.children().length;
};
}
main.service("utilities",[utilities]);
main.controller("Prova",["$scope","utilities",function($scope,utilities){
var self = this;
self.consoleScope = function(){
return utilities.consoleScope($scope);
};
}]);
But even if it runs without errors, it doesn't retrieve the information I wanted. I can comprehend this is not the right way to do this, but I can't see any other way. What could I try?
So you've mixed up your application logic with your DOM logic. Ideally when talking about children you'd be assigning or creating these based upon a data set or collection.
e.g.
//In your controller
$scope.data = someDataSet;
from there you would implement your algorithm based upon your data set.
//still in your controller
$scope.algorithm = function(data){
... implement your logic ...
// e.g.
return data.length > 5;
}
Now in your UI mark up you would use ng-class and an expression to assign the class on any element that needs the class. Your controller shouldn't know about your classes.
<div ng-class="(algorithm(data)) ? 'trueClass' : 'falseClass'" ></div>
This is a really simple implementation but you can extend it pretty easily.
https://docs.angularjs.org/api/ng/directive/ngClass

backbone render this.$el.html(this.template(data)) slow. what to do?

I have a collection to show.
1. for each model in collection, create a View
2. append view.render().el
I find view.render() takes long, more specifically
this.$el.html(this.template(data)) part.
I need to speed things up and found 'DOM manipulation' is slow.
So I looked for the ways to batch process the rendering but didn't find much.
Question 1.
I wonder if there is a way to batch process and attach the final html to the DOM without attaching each row to the DOM?
(I suspect this.$el.html() does the DOM manipulation. If so, can I somehow not perform the this.$el.html() call in view.render() and later assign the view's el to decrease the DOM interaction?)
Question 2.
Are there other pitfalls or performance blocker when redering views in clients?
edit
addAll: function() {
this.$('#thread-loop').html('');
var fragment = document.createDocumentFragment();
for (var i = 0; i < this.threads.length; i++) {
console.log('adding one');
var thread = this.threads.at(i);
var View = this.threadTypeToViewMap[thread.get('thread_type')];
var view = new View( {model: thread, forumSelector: this.forumSelector} );
fragment.appendChild(view.render().el);
}
this.$el.find('#thread-loop')[0].appendChild(fragment);
// this.threads.each(this.addOne, this);
},
Actually, I realised I'm using the fragment technique.
I nailed down the problem more and it looks like when javascript object has lengthy property (user created data), handlebar takes long to find the property (or so I suspect) in template() call.
Take a look at http://marionettejs.com
They extend Backbone to provide views that are optimized for collections (so you can create the DOM nodes in a loop and only add them to the document after you are done). This is going to really help you with performance.
As for question 2, the things that will give you pitfalls are:
Extensive DOM manipulation
Compiling templates on the client instead of the server (i.e. making individual requests for each template after loading your page)
General JavaScript performance bottlenecks (e.g. using polyfill foreach loops using jQuery or underscore instead of native JS, etc.)
Backbone template renders are slow because by default, they use JavaScript's with structure to scope the variable names properly as you expect. A little-known feature of Backbone templates allows you to skip this rather large performance penalty by assigning a variable to put all your data in (typically just "data"):
var t = _.template('<%= data.x %>', null, {variable: 'data'});
template({x: 42});
instead of the normal:
_.template('<%= x %>');
template({x: 42});
Do this, and you will see huge performance gains. This will probably solve your performance issues immediately.
Benchmark: http://jsperf.com/underscore-template-function-with-variable-setting
For Q1 ->
If your $el is not in the dom already, you can append all the $el's into a Document Fragment and then inject the Document Fragment into the page at once. JQuery's John Resig has written a nice writeup about how document fragments work : http://ejohn.org/blog/dom-documentfragments
A couple of things you could try on top of using the doc fragment:
1- If you want to use the for loop, cache the length prior to looping. var threadLength = this.threads.length;
2- That said, I would opt for using _.each instead of the for loop.
3- if you do 2, the threads model should be easily accessed. I don't know if there is any hit to using get(), but you could just access the model.attributes.thread_type directly.
3- Is your template cached?
4- Why are you changing the html of #thread-loop twice? Is that needed? If you need to access it twice, then cache that selector also.

Get rid of surrounding elements in Backbone.Marionette

I have the following view:
return Marionette.ItemView.extend({
el: '<section>',
template: JST['app/scripts/templates/grid.ejs'],
that is called like this:
// a Layout
regions: {
grid: '#grid',
detail: '#detail'
},
onShow: function () {
var detailModel = new DetailModel();
var g = new GridView(detailModel);
this.grid.show(g);
}
The question is: How do I get rid of the surrounding section element ? I tried to omit the el property but that gives me the following strange looking div:
<div productname>
Regards Roger
The surrounding element is required for backbone to work. It is essentially a container/placeholder for your view to sit in, whether its contents have been rendered or not.
If you really insist on not having the container then I would consider resorting to the following:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#set-how-views-el-is-attached
Marionette.Region.prototype.open = function(view){
this.$el.empty().append(view.$el.children());
}
I say 'resorting' because, in my opinion, this is not how Backbone is supposed to be used and may have side-effects. (im not quite sure what will happen when the view in that region tries to re render; what will it's el element be pointing to?)
To expand on Scott's answer, it's probably a very bad idea to try and force the removal of the surronding view tags.
All Backbone views are contained within an DOM element. Given this fact, you have 2 main options:
have Backbone put your view into the default div element
specify which element you want Backbone to wrap your view with, using the el or tagName attributes
If the "extra" tags are creating issues (e.g. you need to generate a specific HTML set for use with a plugin), then you're not defining the wrapping element properly. For more on the subject, take a look at this blog post of mine: http://davidsulc.com/blog/2013/02/03/tutorial-nested-views-using-backbone-marionettes-compositeview/
Edit based on jsFiddle: the reason for your strange behavior is that you were passing a model instance to the initialize function. This is then interpreted as attributes for the view and get set as HTML attributes.
The correct way to provide a model instance to a view is :
new App.FooterView({
model: new App.Model()
})
In other words, you provide a javascript object to the view, with a model property. If you want to learn Marionette basics quickly, check out the free preview to my book: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf (You'll find how to instantiate a view with a model on pages 15-21)

Cleanest way to destroy every Model in a Collection in Backbone?

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());

Can Backbone render a collection in reverse 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.

Resources