Adding a list of views in backbone correctly - backbone.js

Currently when I insert a list of views in backbone I do :
var jqGuestList = $("#guestList");
jqGuestList.empty();
items.each(function(item){
var view = new wedapp.view.GuestItem({
model: item,
collection: this.collection
});
jqGuestList.append(view.render().el);
});
This however cause my a great deal of pain, adding each one manually to the DOM is slow as hell, specially on mobile but even on desktop..
is there a way to insert all the views in one jqGuestList.html(views) instead?

You could use a Document Fragment http://ejohn.org/blog/dom-documentfragments/
var jqGuestList = $("#guestList");
jqGuestList.empty();
var frag = document.createDocumentFragment();
items.each( function(item) {
var view=new wedapp.view.GuestItem({model:item});
frag.appendChild(view.render().el);
});
jqGuestList.append(frag);
You should see some improvement.

Related

Multiple collections tied to one base collection with filters and eventing

I have a complex model served from my back end, which has a bunch of regular attributes, some nested models, and a couple of collections.
My page has two tables, one for invalid items, and one for valid items. The items in question are from one of the nested collections. Let's call it baseModel.documentCollection, implementing DocumentsCollection.
I don't want any filtration code in my Marionette.CompositeViews, so what I've done is the following (note, duplicated for the 'valid' case):
var invalidDocsCollection = new DocumentsCollection(
baseModel.documentCollection.filter(function(item) {
return !item.isValidItem();
})
);
var invalidTableView = new BookIn.PendingBookInRequestItemsCollectionView({
collection: app.collections.invalidDocsCollection
});
layout.invalidDocsRegion.show(invalidTableView);
This is fine for actually populating two tables independently, from one base collection. But I'm not getting the whole event pipeline down to the base collection, obviously. This means when a document's validity is changed, there's no neat way of it shifting to the other collection, therefore the other view.
What I'm after is a nice way of having a base collection that I can have filter collections sit on top of. Any suggestions?
I fleshed out my previous attempt and have come up with an extension to Backbone.Collection that does what I need.
collections.FilteredCollection = Backbone.Collection.extend({
initialize: function(items, options) {
if (_.isUndefined(options.baseCollection))
throw "No base collection to watch";
if (!_.isFunction(options.filterFunc)) {
throw "No filter to apply";
}
_.extend(this, options);
this.listenTo(this.baseCollection, 'all', this.reraise);
},
reraise: function (event) {
this.reset(this.baseCollection.filter(this.filterFunc), { silent: true });
var args = [].slice.call(arguments, 1);
this.trigger(event, args);
}
});
The one small issue I have with this is that I have to manually apply filterFunc to the baseCollection, then pass that in as the items parameter when instantiating a FilteredCollection, but that's something I can live with.
The below code is what I'm using to instantiate. Note that there's another almost-exact copy which is for the collection of ONLY VALID items, but any filters can be applied.
var allDocs = theModel.get('Documents');
var invalidOptions = {
baseCollection: allDocs,
filterFunc: function(item) {
return !item.isValidItem();
}
};
var invalidDocs = allDocs.filter(invalidOptions.filterFunc);
var invalidDocsCollection = new collections.FilteredCollection(
invalidDocs, invalidOptions
);

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

Managing views with routers in Backbone.js

I am working in a single page app that has the following layout:
I am using a Backbone.js router to manage the elements that load on the screen:
var AppRouter = Backbone.Router.extend({
routes: {
"" : "list",
"content1" : "content1",
"content1/cont3": "cont3"
},
list: function() {
var list = new List().render().$el; //view
$("#List").html(list);
},
content1: function(){
var cont1 = new Content1().render().$el; //view
$("#Content1").html(cont1);
},
content3: function(){
var cont3 = new Content3().render().$el; //view
$("#Cont3").html(cont3);
}
});
Everytime I click on a list item in #List, #Content1 gets generated, and when I do it on the blocks on #Content1, #Cont3 appears.
The problem I am facing is that if, for some reason, I refresh the page when the adress is localhost/content1, for example; the elements in #List disappear.
I want the content in #List to be always present when loaded, independent on what the url may be, as well as the content in #Content1. Is there a way to achieve this using backbone routers?
Thanks
You don't need to match 1 route <-> 1 View.
I would change the routes to something like this:
"list": "displayContent",
"list/:c1": "displayContent",
"list/:c1/:c3": "displayContent",
So it's only one callback the one who organize what Views are rendered or what not.
displayContent: function(c1,c3) {
Store the content1 variable, and check if has been rendered already or not, etc.
Have a look to this question: How to handle initializing and rendering subviews in Backbone.js?
Or for more complex apps, maybe a layout framework in top of Backbone could help, although I would recommend do your own stuff until you understand how Backbone works.
https://github.com/tbranyen/backbone.layoutmanager
https://github.com/derickbailey/backbone.marionette
I think you can try this:
.....
list: function() {
var list = new List().render().$el; //view
$("#List").html(list);
},
content1: function(){
var cont1 = new Content1().render().$el; //view
$("#Content1").html(cont1);
//Pseudo code
if (listisempty){
list();//If the list is empty, then the initialization list.
};
},
.....

Can you set attributes on Backbone.js Collections? If so, how?

I am just getting into backbone.js and I thought the best way to get into it is to actually build a todo list (yea...pretty original). Humor aside, I have searched google and the docs and stackoverflow of course, for a way to add an attribute to a collection. So, in my case a todo list is a collection of listitems. However a Todo List can have a title as according to my design, I want to be able to create multiple lists.
var TodoList = Backbone.Collection.extend({
model: ListItem
});
//is this possible for collections?
var newTodoList = new TodoList({name: "My first list"});
Thanks a lot for the help! Appreciate it!
Yes, it's possible. Look at the signature of the Collection constructor:
new Collection([models], [options])
So you could write like this:
var ListItem = Backbone.Model.extend({});
var TodoList = Backbone.Collection.extend({
model: ListItem,
initialize: function(models, options) {
options || (options = {});
if (options.title) {
this.title = options.title;
};
}
})
var iPromise = new TodoList([], {
title: 'NY Resolutions'
})
console.log(iPromise.title);

cached view losing its events

I render a collection of models, which is associated with a collectionView where when rendered each element in the collection has its own 'itemview' which is rendered.
When a collection is sorted and the listView re-rendered based on the new order, I had been creating a totally new view for each item, and as I was not clearing up any previous instances of views associated with that model, I believe zombies being left around.
So initially rendering my collection I would do...
render : function() {
$(this.el).empty();
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view = new TB_BB.RequestItemView({model : model});
els.push(view.render().el);
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So whenever the render function was called a second/third etc time, I had not cleared up the TB_BB.RequestItemView (hence the zombies)
To overcome this I tried to add some simple caching in the collections view, so that instead of creating a new itemview if it had already been created use that instead. My code
initialize : function(){
_.bindAll(this,"render");
this.collection.bind("add", this.render);
this.collection.bind("remove", this.render);
this.template = $("#request-list-template");
this.views = {};
},
events : {
"change #sort" : "changesort",
"click #add-offer" : "addoffer",
"click #alert-button" : "addalert"
},
render : function() {
$(this.el).empty();
outerthis = this;
var content = this.template.tmpl({});
$(this.el).html(content);
sortingView.el ='#sorting-container';
var els = [];
_.each(this.collection.models, function(model){
var view;
if(outerthis.views[model.get('id')]) {
view = outerthis.views[model.get('id')];
} else {
view = new TB_BB.RequestItemView({model : model});
outerthis.views[model.get('id')] = view;
}
});
$('#request-list').append(els);
sortingView.render();
return this;
}
So this works in so much as the views are re-used - however what I have noticed is that if I use a cached view (e.g. the collection has been sorted and the render function finds a cached view) that all of the events on the sub itemview stop working? why is that?
Also could anyone suggest a better way of doing this?
You can use delegateEvents ( http://documentcloud.github.com/backbone/#View-delegateEvents ) to bind the events again.
As OlliM mentioned the reason is because the events are bound to the dom element, but instead of rebinding the element you can also just detach them instead of removing them (detach keeps the event bindings http://api.jquery.com/detach/)
something like
var $sortContainer = $('#sorting-container');
$('li', $sortContainer).detach();
And then just reattach the element
$cnt.append(view.el);
I would also consider using a document fragment while rebuilding/sorting the list and then attaching appending that instead.

Resources