Backbone Marionette Slow Composite Views (200+ collections) - backbone.js

When showing a dropdown composite view collection of around 200 countries my application gets far too slow.
What is the best way to increase performance when dealing with large collections in marionette composite views?
Here is the function in the controller that is very slow to load. It is fast with only the following lines removed:
#layout.shippingCountryRegion.show shippingCountryView
#layout.billingCountryRegion.show billingCountryView
So it appears to be a very slow rendering issue.
Show.Controller =
showProfile: ->
#layout = #getLayoutView()
#layout.on "show", =>
headerView = #getHeaderView()
#layout.headerRegion.show headerView
accessView = #getAccessView()
#layout.accessRegion.show accessView
billingReadmeView = #getBillingReadmeView()
#layout.billingReadmeRegion.show billingReadmeView
billingFieldsView = #getBillingFieldsView()
#layout.billingFieldRegion.show billingFieldsView
shippingReadmeView = #getShippingReadmeView()
#layout.shippingReadmeRegion.show shippingReadmeView
shippingFieldsView = #getShippingFieldsView()
#layout.shippingFieldRegion.show shippingFieldsView
MyApp.request "location:get_countries", (countries) =>
billingCountryView = #getBillingCountryView(countries)
#layout.billingCountryRegion.show billingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('billing_country_id'), (states) =>
billingStateView = #getBillingStatesView(states)
#layout.billingStateRegion.show billingStateView
MyApp.request "location:get_countries", (countries) =>
shippingCountryView = #getShippingCountryView(countries)
#layout.shippingCountryRegion.show shippingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('shipping_country_id'), (states) =>
shippingStateView = #getShippingStatesView(states)
#layout.shippingStateRegion.show shippingStateView
MyApp.mainRegion.show #layout
The billing country view:
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
onRender: ->
this.$el.attr('value', this.model.get('id'));
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
this.$el.attr('selected', 'selected');
class View.BillingCountryDropdown extends MyApp.Views.CompositeView
template: billingCountryTpl
itemView: View.BillingCountryDropdownItem
itemViewContainer: "select"
The template, simply:
<label>Country
<select id="billing_country_id" name="billing_country_id">
<%- name %>
</select>
</label>

Your code can be optimized. Just move content of onRender method to the ItemView attributes.
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
attributes: ->
var id = this.model.get('id');
var attributes = { 'value': id };
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
attributes['selected'] = 'selected';
return attributes
The difference between this method and onRender case is that, on render will execute when collection already rendered and 200+ operations will be done with DOM nodes, which will bring performance issues.
In case of attributes method, it executes upon view creation.

There are few advices you can follow:
1) Ask your self do you really need to render all items at once? Maybe you can render part of collection and render other items on scroll or use pagination or use 'virtual scrioll' with SlickGrid or Webix for example.
2) Checkout how often you re-render your view. Try to minify num of events cause re-render
3) Try to minify num of event listeners of ItemView. Its good practice to delegate context events to CollectionView
4) You can use setTimeout to render collection by parts. For example you divide you coll in 4 parts by 50 items and raise 4 timeouts to render it.
5) You can optimize underscore templating and get rid of with {} operator. http://underscorejs.org/#template

What's in your 'billingCountryItemTpl' varible? If it's just string with template ID then you could precompile your template using Marionette.TemplateCache.
So you'll have:
template: Marionette.TemplateCache.get(billingCountryItemTpl)

Related

How to organize Backbone collection with a specific selection?

I have a collection of items. I would like to keep track of the current selection. When the user clicks on a different item in the collection, I want to indicate that the item is selected and display the details of the selected item. Think of this as a list with a detail view (like a typical email client).
Example of a master-detail layout (source):
I currently have something like this (written in CoffeeScript, templates use haml-coffee):
class Collections.Items extends Backbone.Collection
model: Models.Item
setCurrentSelection: (id)->
# what to do here? Is this even the right way to do it?
getCurrentSelection: ->
# what to do here? Is this even the right way to do it?
class Views.ItemsPage extends Backbone.View
list_template: JST['items/list']
details_template: JST['items/details']
events:
'click .item': 'updateSelection'
initialize: (options)->
#collection = options.collection
render: ->
$('#items_list').html(#list_template(collection: #collection.toJSON())) # not sure if this is how to render a collection
$('#item_details').html(#details_template(item: #collection.currentSelection().toJSON())) # how to implement currentSelection?
#
updateSelection: (event)->
event.preventDefault()
item_id = $(event.currentTarget).data('id')
# mark the item as selected
# re-render using the new selection
# templates/items/list.hamlc
%ul
- for item in #collection
%li{data:{id: item.id}, class: ('selected' if item.selected?)} # TODO: How to check if selected?
= item.name
# templates/items/details.hamlc
%h2= #item.name
I'm not sure if I'm following you (my CoffeeScript is a bit rusty), but I think what you're trying to do is set a selected property on the appropriate model in your updateSelection method, and then re-render your view.
In other words:
updateSelection: (event)->
event.preventDefault()
item_id = $(event.currentTarget).data('id')
model = this.collection.get(item_id) # get the model to select
model.selected = true # mark the item as selected
this.render() # re-render using the new selection
even saying "my CoffeeScript is a bit rusty" is too much for me. But i'll still attempt to explain as best as i can in js.
First the backbone way is to keep models as a representation of a REST resource document. (server side - persisted data).
Client side presentation logic should stick to views. to remember which list item is visible in in the details part is job of the that specific view. initiating change request for details view model is job of the list of items.
the ideal way is to have two separate views for list and details. (you can also go a bit more ahead and have a view for every item in the list view.
parent view
var PageView = Backbone.View.extend({
initialize: function() {
//initialize child views
this.list = new ItemListView({
collection : this.collection //pass the collection to the list view
});
this.details = new ItemDetailView({
model : this.collection.at(1) //pass the first model for initial view
});
//handle selection change from list view and replace details view
this.list.on('itemSelect', function(selectedModel) {
this.details.remove();
this.details = new ItemDetailView({
model : selectedModel
});
this.renderDetails();
});
},
render: function() {
this.$el.html(this.template); // or this.$el.empty() if you have no template
this.renderList();
this.renderDetails();
},
renderList : function(){
this.$('#items_list').append(this.list.$el); //or any other jquery way to insert
this.list.render();
},
renderDetails : function(){
this.$('#item_details').append(this.details.$el); //or any other jquery way to insert
this.details.render();
}
});
list view
var ItemListView = Backbone.View.extend({
events : {
'click .item': 'updateSelection'
},
render: function() {
this.$el.html(this.template);
this.delegateEvents(); //this is important
}
updateSelection : function(){
var selectedModel;
// a mechanism to get the selected model here - can be same as yours with getting id from data attribute
// or you can have a child view setup for each model in the collection. which will trigger an event on click.
// such event will be first captured by the collection view and thn retriggerd for page view to listen.
this.trigger('itemSelect', selectedModel);
}
});
details view
var ItemDetailView = Backbone.View.extend({
render: function() {
this.$el.html(this.template);
this.delegateEvents(); //this is important
}
});
This won't persist the state through routes if you don't reuse your views. in that case you need to have a global state/event saving mechanism. somthing like following -
window.AppState = {};
_.extend(window.AppState, Backbone.Events);
//now your PageView initilize method becomes something like this -
initialize: function() {
//initialize child views
this.list = new ItemListView({
collection : this.collection //pass the collection to the list view
});
var firstModel;
if(window.AppState.SelectedModelId) {
firstModel = this.collection.get(window.AppState.SelectedModelId);
} else {
firstModel = this.collection.at(1);
}
this.details = new ItemDetailView({
model : firstModel //pass the first model for initial view
});
//handle selection change from list view and replace details view
this.list.on('itemSelect', function(selectedModel) {
window.AppState.SelectedModelId = selectedModel.id;
this.details.remove();
this.details = new ItemDetailView({
model : selectedModel
});
this.renderDetails();
});
}
EDIT
Handling selected class (highlight) in list view . see comments for reference.
list view template -
<ul>
<% _.each(collection, function(item, index){ %>
<li data-id='<%= item.id %>'><%= item.name %></li>
<% }); %>
</ul>
inside list view add following method -
changeSelectedHighlight : function(id){
this.$(li).removeClass('selected');
this.$("[data-id='" + id + "']").addClass('selected');
}
simply call this method from updateSelection method and during PageView initialize.
this.list.changeSelectedHighlight(firstModel.id);

How to display an item view with no tag

The problem is I want to render option elements with the value attribute set as well as the text node.
So setting tagName to option in my ItemView does not just do this. The solution I have at the moment is to set it to option and then use the following code in the ItemView constructor to set the value attibute:
onRender: function () {
this.$el.attr('value', this.model.get('name'));
}
This works.
But is there any other way?
What I'd really like to do is just tell Marionette not to output an element at all and then in my ItemView template have this:
<option value="<%= name %>"> <%= name %> </option>
Is this possible?
It's possible but a bit fiddly, by default Backbone always uses a wrapper element (definable by using tagName) but you'd have to expressly populate the attributes (as you are doing above).
It is however possible to circumvent the wrapper element using a slightly convoluted setElement approach and this will enable you keep all markup in a template with attributes on the root node populated from the model. I like this approach as well since I personally think it keeps a clearer separation of concerns.
Take a look here for an example - Backbone, not "this.el" wrapping
Not sure if Marionette has a build in mechanism for doing this.
I am not sure you should from all different kind of reasons.
But I understand that in the use case of a single tag view, the way Marionette/Backbone work you either create another tag and have a view, or you use onRender and query to update the single tag that was already generated for you.
You could have this Object that will extend ItemView, lets call it SingleTagView. Basicly I am extending ItemView and using overrunning it's render function with one change.
Instead of doing:
this.$el.html(html);
I am doing:
var $html = $(html);
this.$el.replaceWith($html);
this.setElement($html);
Here is the code:
var SingleTagView = Marionette.ItemView.extend({
render: function() {
this.isClosed = false;
this.triggerMethod("before:render", this);
this.triggerMethod("item:before:render", this);
var data = this.serializeData();
data = this.mixinTemplateHelpers(data);
var template = this.getTemplate();
var html = Marionette.Renderer.render(template, data);
// above is standart ItemView code
var $html = $(html);
this.$el.replaceWith($html);
this.setElement($html);
// this code above replaced this.$el.html(html);
// below is standart ItemView code
this.bindUIElements();
this.triggerMethod("render", this);
this.triggerMethod("item:rendered", this);
return this;
}
});
If you are going to use this kind of angle, make sure you test the behavior properly (event handling, Dom leaks, different flows with before:render events).
I would try:
var optionTag = Marionette.ItemView.extend({
tagName: "option",
initialize: function(){
this.attributes = {
value: this.model.get('name');
}
}
}
Backbone should then do the rest (i.e. putting the content of attributes into the HTML tag)...

Backbone.js - Build view with sub models

I have an edit view for a Backbone Model that I create each time the the element is clicked on. The problem I have is that the edit view needs two Backbone collections to create the edit form (it contains two <select> lists).
The view:
MyApp.elementView = Backbone.View.extend({
events: {
'click .edit': 'editForm',
},
editForm: function(ev) {
var editView = new TimeTrack.Views.EditJob({
model: this.model
// This view needs two more collections
// for the <select> elements
});
...
}
});
Instantiate the view:
var elementView = new MyApp.elementView({
collection: elementCollection
});
What is the best way to push the needed collections to the edit view? Do I have to pass the collections need for the edit view from the elementView form the instantiation? Or is there a better way of doing this?
I did so, passed in view 2 collections, 1 - the main and the other as follows:
to elementView - second collection and in elementView recive her.
example:
in router I'm
initialize: ->
(YourNameSpace).secondCollection = new (YourNameSpace).secondCollection
elements: =>
view = new (YourNameSpace).elementView( secondCollection: #secondCollection )
$('.l-yield').html(view.render().el)

In Backbone.js how do you handle Ordinal Numbering and changing numbering based on jQuery Sortable in the view?

Here is my situation. I have a bunch of "Question" model inside a "Questions" collection.
The Question Collection is represented by a SurveyBuilder view.
The Question Model is represented by a QuestionBuilder view.
So basically you have an UL of QuestionBuilder views. The UL has a jQuery sortable attached (so you can reorder the questions). The question is once I'm done reordering I want to update the changed "question_number"s in the models to reflect their position.
The Questions collection has a comparator of 'question_number' so collection should be sorted. Now I just need a way to make their .index() in the UL reflect their question_number. Any ideas?
Another problem is DELETEing a question, I need to update all the question numbers. Right now I handle it using:
var deleted_number = question.get('question_number');
var counter = deleted_number;
var questions = this.each(function(question) {
if (question.get('question_number') > deleted_number) {
question.set('question_number', question.get('question_number') - 1);
}
});
if (this.last()) {
this.questionCounter = this.last().get('question_number') + 1;
} else {
this.questionCounter = 1;
}
But it seems there's got to be a much more straighforward way to do it.
Ideally whenever a remove is called on the collection or the sortstop is called on the UL in the view, it would get the .index() of each QuestionuBuilder view, update it's models's question_number to the .index() + 1, and save().
My Models,Views, and Collections: https://github.com/nycitt/node-survey-builder/tree/master/app/js/survey-builder
Screenshot: https://docs.google.com/file/d/0B5xZcIdpJm0NczNRclhGeHJZQkE/edit
More than one way to do this but I would use Backbone Events. Emit an event either when the user clicks something like done sorting, hasn't sorted in N seconds, or as each sort occurs using a jQuery sortable event such as sort. Listen for the event inside v.SurveyBuilder.
Then do something like this. Not tested obviously but should get you there relatively easily. Update, this should handle your deletions as well becuase it doesn't care what things used to be, only what they are now. Handle the delete then trigger this event. Update 2, first examples weren't good; so much for coding in my head. You'll have to modify your views to insert the model's cid in a data-cid attribute on the li. Then you can update the correct model using your collection's .get method. I see you've found an answer of your own, as I said there are multiple approaches.
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.eventHub = EventHub;
this.questions = new c.Questions();
this.questions.on('add', this.addQuestion, this);
this.eventHub.on('questions:doneSorting', this.updateIndexes)
},
updateIndexes: function(e) {
var that = this;
this.$('li').each(function(index) {
var cid = $(this).attr('data-cid');
that.questions.get(cid).set('question_number', index);
});
}
I figured out a way to do it!!!
Make an array of child views under the parent view (in my example this.qbViews maintains an array of QuestionBuilder views) for the SurveyBuilder view
For your collection (in my case this.questions), set the remove event using on to updateIndexes. That means it will run updateIndexes every time something is removed from this.questions
In your events object in the parent view, add a sortstop event for your sortable object (in my case startstop .question-builders, which is the UL holding the questionBuilder views) to also point to updateIndexes
In updateIndexes do the following:
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
And there is my full code for SurveyBuilder view:
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.qbViews = []; //will hold all of our QuestionBuilder views
this.questions = new c.Questions(); //will hold the Questions collection
this.questions.on('add', this.addQuestion, this);
this.questions.on('remove', this.updateIndexes, this); //We need to update Question Numbers
},
bindSortable: function() {
$('.question-builders').sortable({
items: '>li',
handle: '.move-question',
placeholder: 'placeholder span11'
});
},
addQuestion: function(question) {
var view = new v.QuestionBuilder({
model: question
});
//Push it onto the Views array
this.qbViews.push(view);
$('.question-builders').append(view.render().el);
this.bindSortable();
},
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
events: {
'click .add-question': function() {
this.questions.add({});
},
//need to update question numbers when we resort
'sortstop .question-builders': 'updateIndexes'
}
});
And here is the permalink to my Views file for the full code:
https://github.com/nycitt/node-survey-builder/blob/1bee2f0b8a04006aac10d7ecdf6cb19b29de8c12/app/js/survey-builder/views.js

Pagination in Backbone.js

I know that there is a component for this but based on what i see you have to create a new collection with the component extended. Is there another way to do pagination in backbone?
All i need is just a previous and next button limit the items per page to 12. i've been creating it on javascript ( not a good solution for production environment ). Any ideas?
Since Backbone collection has underscore methods extended, you might want to create helper pagination method very easy. I use something like :
var Paginated = Backbone.Collection.extend({
pagination : function(perPage, page) {
page = page-1;
var collection = this;
collection = _(collection.rest(perPage*page));
collection = _(collection.first(perPage));
return collection.map( function(model) { return model.toJSON() } );
}
});
This returns toJSON of your collection, you may play with it in the jsfiddle : http://jsfiddle.net/YHmrp/2/

Resources