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.
Related
so i have a situation where in i'm creating html using jquery. I have no choice in this matter since I'm forced to use an old jquery plugin and integrate the created html in angular and the only way to do that is $compile. basically the flow is
var createbody = function(htmlcontents,scope){
$compile(htmlcontents)(scope);
}
the company internal jquery plugin is some sort of endless scroll for a table where it destroys and recreates a chunk of the tr's depending on the scroll position. so everytime you scroll, if the plugin needs to destroy and add tr's, createbody gets called.
the problem is that the scroll gets really laggy whenever it does the destroy and create part because of the compile. a directive is not an option at this point.
question: is there a way to cache the previously compiled chunk and use that later on whenever the plugin decides it needs to use that chunk again.? thanks
You can reuse compiled template:
var compiledTemplate = $compile(html);
compiledTemplate($scope1);
compiledTemplate($scope2);
compiledTemplate($scope3);
You can also see how ngRepeat reuses elements: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L469, but it's a bit more complicated.
I'd like to implement an infinite scroll, but without call a load more function everytime.
Let's think I've download an array of 1000 items from Parse. I'd like to show 10 items and then add more and more items to my list using ng-repeat from a local array.
I think the best solution is to implement a directive, but maybe there is something already done...
Anyone?
Sure you could do it as a directive, but regardless, you are going to need some sort of loadMore function. To put in other words, you are going to need to detect when the user has scrolled to a particular position, and perform some updating function.
While your own directive might be better at encapsulating your specific requirement, you could achieve what you want with any other current infinite scrolling plugin. Simply keep two versions of your array in your model. One is the data, while the other is the data that the user sees. If your arrays are of objects, then both should be able to work on the same items.
For example:
var data = [...];
var position = 0;
var pageSize = 10;
$scope.viewable = [];
$scope.loadMore = function(){
$scope.viewable = $scope.viewable.concat(data.slice(position, position + pageSize));
position += pageSize;
};
Update:
If this is something you need to implement often, but don't want to implement your own scrolling related directive, you could encapsulate the idea above in a factory that manages multiversion arrays. You would still have to hook the loadMore, and setup the multiversion, but your controller and view code would look like the following:
// the data you loaded somewhere
var data = [...];
$scope.specialArray = PageableArrayFactory.create(data);
// then in your html
ng-repeat="item in specialArray.viewItems"
// where you put your infinite scroll
infinite-scroll="specialArray.loadMore()"
PageableArrayFactory just needs to be a factory that takes in your big data array, and keeps track of a viewable copy array like my initial example. It shouldn't be too hard to implement, and can then be reused with a single line of code in any controller after you load your data.
Another example:
You could also build a custom filter on $index (or use ng-show/if), so that ng-repeat only shows the items that you want. You will still need to hook loadMore() though if you want to use existing infinite scroller code, which means you will still need something my factory example.
Matt's solution is perfectly fine. Especially considering you've mentioned you want to use ng-repeat and you want to paginate your result.
It doesn't seem to be a huge effort to implement what he's suggesting. The other option is to use collection-repeat, especially for performances.
The implementation is very simple:
<ion-content>
<ion-item collection-repeat="item in items">
{{item}}
</ion-item>
</ion-content>
and you don't have to be worried about loading all the items together as the framework will be responsible to load chunks of information for you.
The only drawback - as far as I am aware - is you cannot control the number of items you want to display.
I am trying to test drive a view event using Jasmine and the problem is probably best explained via code.
The view looks like:
App.testView = Backbone.View.extend({
events: { 'click .overlay': 'myEvent' },
myEvent: function(e) {
console.log('hello world')
}
The test looks something like:
describe('myEvent', function() {
it('should do something', function() {
var view = new App.testView();
view.myEvent();
// assertion will follow
});
});
The problem is that the view.myEvent method is never called (nothing logs to the console). I was trying to avoid triggering from the DOM. Has anyone had similar problems?
(Like I commented in the question, your code looks fine and should work. Your problem is not in the code you posted. If you can expand your code samples and give more info, we can take another look at it. What follows is more general advice on testing Backbone views.)
Calling the event handler function like you do is a legitimate testing strategy, but it has a couple of shortcomings.
It doesn't test that the events are wired up correctly. What you're testing is that the callback does what it's supposed to, but it doesn't test that the action is actually triggered when your user interacts with the page.
If your event handler needs to reference the event argument or the test will not work.
I prefer to test my views all the way from the event:
var view = new View().render();
view.$('.overlay').click();
expect(...).toEqual(...);
Like you said, it's generally not advisable to manipulate DOM in your tests, so this way of testing views requires that view.render does not attach anything to the DOM.
The best way to achieve this is leave the DOM manipulation to the code that's responsible for initializing the view. If you don't set an el property to the view (either in the View.extend definition or in the view constructor), Backbone will create a new, detached DOM node as view.el. This element works just like an attached node - you can manipulate its contents and trigger events on it.
So instead of...
View.extend({el: '#container'});
...or...
new View({el:'#container'});
...you should initialize your views as follows:
var view = new View();
$("#container").html(view.render().el);
Defining your views like this has multiple benefits:
Enables testing views fully without attaching them to DOM.
The views become reusable, you can create multiple instances and render them to different elements.
If your render method does some complicated DOM manipulation, it's faster to perform it on an detached node.
From a responsibility point of view you could argue that a view shouldn't know where it's placed, in the same way a model should not know what collection it should be added to. This enforces better design of view composition.
IMHO, this view rendering pattern is a general best practice, not just a testing-related special case.
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());