CompositeView and Waiting images to be loaded - backbone.js

I have implemented a marionette region that draws columns depending on the screen size, so i have variable number of columns. The problem that i have is that i want to locate the itemView in the column with the smallest height. But the CompositeView appendHtml method seems to append all itemViews before a i can calculate correctly the height of the column. The ItemView has a image and is not rendered yet,so im not calculating correctly the height.
I paste my aproximation:
appendHtml: (collectionView, itemView, index) ->
$("#column-container").waitForImages ->
smallest_column = 0
columns= $("#column-container").children().length
column_sizes = []
for i in [0...columns]
column_sizes[i] = $($("#column-container").children()[i]).height()
smallest_column = column_sizes.indexOf(Math.min.apply(Math,column_sizes))
target = $($("#column-container").children()[smallest_column])
target.append(itemView.el)
I have watched that this method appends all the collection items and this could be one reason to do this logic in other place.
PD: I dont want to use masonry.

I have tried to deal with this a number of ways, the most practical I have found is to simply set a timer that fires lets say every half second to set the height of columns that I what to control.
So for instance If I have a number of columns and I all want them to be the same height (the height of the tallest) then I would do something like this.
Here is a small module that I use for this in my views:
define([
'jquery',
'underscore'
],
function($, _){
var tools = {
makeColHeightSame: function(col_array, min_height) {
var max_col_height = 0;
_.each(col_array, function(col, index, cols){
$(col).removeAttr('style');
var height = $(col).height();
if (height > max_col_height) {
max_col_height = height;
}
}, this);
_.each(col_array, function(col, index, cols){
if (_.isUndefined(min_height)) {
$(col).height(max_col_height);
} else {
if (max_col_height < min_height) {
$(col).height(min_height);
} else {
$(col).height(max_col_height);
}
}
}, this);
}
};
return tools;
});
I my view I do the following:
initialize: function(){
// Set up resize timer
this.resize_timer = setInterval(_.bind(this.resizeCols, this), 500);
}
When the view is destroyed:
onBeforeDestroy: function() {
clearInterval(this.resize_timer);
},
And my Resize Cols function is:
resizeCols: function() {
ViewTools.makeColHeightSame(this.$('.registration_box_bottom'));
ViewTools.makeColHeightSame(this.$('.opportunities_block'));
},
I am using underscore.js to make sure that the this context is set to my view when the timer fires and the using require.js to bring in the ViewTools module.

Related

Marionette CompositeView Sort Rendering

I have a Marionette (2.4.1) CompositeView and when I do a sort it re-renders the entire view rather than the childView. The header icons revert back. I could fix them on render but is there a way that I can just render the childView?
diaryEntries = Backbone.Marionette.CompositeView.extend({
template : diaryEntries,
className: 'diary-entries',
collection: new Diary(),
childViewContainer: 'tbody',
reorderOnSort: true,
events: {
'click th[data-sort]': 'sort',
'click .pagination a': 'paginate'
},
initialize: function() {
this.itemsPerPage = 5;
this.currentPage = 1;
this.pages;
},
...
sort: function(e) {
var $th, dir, sort, sorted;
e.preventDefault();
$th = $(e.currentTarget);
sort = $th.data('sort');
if (sort === this.collection.sortField) {
this.collection.sortDirection *= -1;
} else {
this.collection.sortDirection = 1;
}
this.collection.sortField = sort;
$('span.glyphicon').removeClass('active-sort');
$th.siblings('th').find('span.glyphicon').removeClass('glyphicon-chevron-down glyphicon-chevron-up').addClass('glyphicon-sort');
if (this.collection.sortDirection === 1) {
$th.find('span.glyphicon').removeClass('glyphicon-chevron-down glyphicon-sort').addClass('glyphicon-chevron-up active-sort');
} else {
$th.find('span.glyphicon').removeClass('glyphicon-chevron-up glyphicon-sort').addClass('glyphicon-chevron-down active-sort');
}
this.collection.sort();
},
...
});
Well, looks like Marionette was concerned about the same thing you are. I couldn't find this in the docs, but it's pretty plain in the source. If you pass this option:
reorderOnSort: true
into your Collection/Composite view, on a 'sort' event the Collection/View will not re render, just its children.
See this line in the Marionette source: https://github.com/marionettejs/backbone.marionette/blob/v2.4.1/src/collection-view.js#L166
UPDATE If you're filtering your children views, running sort on your collection will invoke render on the Collection/CompositeView. The logic is that if you're paginating your children results, then you must sort the original, unfiltered, collection to properly display paginated results.
Nonetheless, I don't see anything intrinsically wrong with paginating a filtered set.
Fortunately, its easy to override the sort method to render whether your results are filtered or not. On you Collection/CompositeView include this method:
reorder: function() {
var children = this.children;
var models = this._filteredSortedModels();
// get the DOM nodes in the same order as the models
var els = _.map(models, function(model) {
return children.findByModel(model).el;
});
this.triggerMethod('before:reorder');
this._appendReorderedChildren(els);
this.triggerMethod('reorder');
}
},

d3.js force layout namespace in a backbone view

I'm working on a medium-complex app using backbone.js to handle wordpress data, and i can't figure out how to get the force working in a backbone layout.
basically, i'm trying to instantiate a force layout within a backbone boilerplate layout, like this:
myLayout = Backbone.Layout.extend({
initialize: function() {
var f = this; // i.e. the layout instance
f.force = d3.layout.force()
.nodes(myModels)
.on("tick", f.tick)
.gravity(0)
.friction(0.9)
.start();
console.log(f.force);
},
tick: function() {
// stuff to do when the force ticks
}
});
The problem is that the force is being defined with all blank functions, like gravity: function(x) { //lots of null things here }. i'm pretty sure it's a namespacing issue, but nothing i try works - i've tried doing $(window).force, var force, $this.force...
in my example tick is the only namespaced function, but i've tried doing that with all the others too (gravity, friction, etc.) to no avail (even though they should just be chaining onto the force object).
anyone have any ideas? i can't really post a .jsfiddle because the app is too complicated, so sorry in advance about that. The current version is up here
edit: here's how d3 can access the models successfully:
this works:
myLayout.nodes = myLayout.d3_wrapper.selectAll(".node")
.data(myModels)
.enter().append("g").attr("class", "node")
.attr("x",10)
.attr("y",10);
myLayout.nodes.append("clipPath")
.attr("id", function(d) { return d.get("slug"); })
as does this:
myLayout.nodes.append("clipPath")
.attr("id", function(d) { return d.attributes.slug });
edit: in the interest of clarity, here's the non-nicknamed code:
setforce: function() { // this gets called from the layout's initialize fn
console.log("setting force");
var f = this; // the layout
f.force = d3.layout.force()
.nodes(Cartofolio.elders.models) // Cartofolio is the module, elders is a Backbone Collection
.gravity(0)
.friction(0.9)
.start();
console.log(f.force);
}
I would try using toJSON() on your collection before passing it to d3:
myLayout = Backbone.Layout.extend({
initialize: function() {
var f = this; // i.e. the layout instance
f.force = d3.layout.force()
.nodes(myModels.toJSON())
.on("tick", f.tick)
.gravity(0)
.friction(0.9)
.start();
console.log(f.force);
},
tick: function() {
// stuff to do when the force ticks
}
});

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

Backbone.js view content keep multiplying instead of clearing

I'am new to Backbone.js and this problem has really got me stumped.
A view is built up from a collection, the collection results are filtered to place each set of results into their own array and then I make another array of the first items from each array, these are the 4 items displayed.
This works fine the first time the page is rendered but when I navigate away from this page and then go back the page now has 8 items, this pattern of adding 4 continues everytime I revisit the page.
// Locatore List Wrapper
var LocatorPageView = Backbone.View.extend({
postshop: [],
postbox: [],
postboxlobby: [],
postboxother: [],
closestPlaces: [],
el: '<ul id="locator-list">',
initialize:function () {
this.model.bind("reset", this.render, this);
},
render:function (eventName) {
//console.log(this)
// Loop over collecion, assigining each type into its own array
this.model.models.map(function(item){
var posttype = item.get('type').toLowerCase();
switch(posttype) {
case 'postshop':
this.postshop.push(item);
break;
case 'postbox':
this.postbox.push(item);
break;
case 'postbox lobby':
this.postboxlobby.push(item);
break;
default:
this.postother.push(item);
}
return ;
}, this);
// Create a closest Places array of objects from the first item of each type which will be the closest item
if (this.postshop && this.postshop.length > 0) {
this.closestPlaces.push(this.postshop[0]);
}
if (this.postbox && this.postbox.length > 0) {
this.closestPlaces.push(this.postbox[0]);
}
if (this.postboxlobby && this.postboxlobby.length > 0) {
this.closestPlaces.push(this.postboxlobby[0]);
}
if (this.postother && this.postother.length > 0) {
this.closestPlaces.push(this.postother[0]);
}
// Loop over the Closest Places array and append items to the <ul> contianer
_.each(this.closestPlaces, function (wine) {
$(this.el).append(new LocatorItemView({
model:wine
}).render().el);
}, this);
return this;
}
})
// Locator single item
var LocatorItemView = Backbone.View.extend({
tagName:"li",
template:_.template($('#singleLocatorTemplate').html()),
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .locator-map": "loadMap"
},
loadMap: function(e) {
e.preventDefault();
// Instantiate new map
var setMap = new MapPageView({
model: this.model,
collection: this.collection
});
var maptype = setMap.model.toJSON().type;
App.navigate('mappage', {trigger:true, replace: true});
setMap.render();
App.previousPage = 'locator';
}
});
window.App = Backbone.Router.extend({
$body: $('body'),
$wrapper: $('#wrapper'),
$header: $('#header'),
$page: $('#pages'),
routes: {
'' : '',
'locator': 'locator'
},
locator:function () {
this.$page.empty(); // Empty Page
this.places = new LocatorPageCollection(); // New Collection
this.placeListView = new LocatorPageView({model:this.places}); // Add data models to the collection
this.places.fetch();
this.$page.html(this.placeListView.render().el); // Append the renderd content to the page
header.set({title: 'Locator'}); // Set the page title
this.$body.attr('data-page', 'locator'); // Change the body class name
this.previousPage = ''; // Set previous page for back button
}
});
All the properties in your Backbone.View.extend argument are attached to the view's prototype. In particular, these properties:
postshop: [],
postbox: [],
postboxlobby: [],
postboxother: [],
closestPlaces: [],
end up attached to LocatorPageView.prototype so each LocatorPageView instance shares the same set of arrays and each time you use a LocatorPageView, you push more things onto the same set of shared arrays.
If you need any mutable properties (i.e. arrays or objects) in your Backbone views, you'll have to set them in your constructor:
initialize: function() {
this.postshop = [ ];
this.postbox = [ ];
this.postboxlobby = [ ];
this.postboxother = [ ];
this.closestPlaces = [ ];
}
Now each instance will have its own set of arrays.
This sounds like a classic Zombie View problem. Basically when you do this:
this.model.bind("reset", this.render, this);
in your view, you never unbind it. Thus, the view object is still bound to the model and can't be removed from memory. When you create a new view and reset, you have that listener still active which is why you see the duplicate view production. Each time you close and redo the view, you're accumulating listeners which is why it increases in multiples of 4.
What you want to do is unbind your listeners when you close out the view and rid your program of binds.
this.model.unbind("reset", this.render, this);
This should eliminate the pesky zombies. I'll add a link with more detailed information when I find it.
UPDATE - added useful references
I also ran into this problem a while back. It's quite the common gotcha with Backbone. #Derick Bailey has a really good solution that works great and explains it well. I've included the links below. Check out some of the answers he's provided in his history regarding this as well. They're all good reads.
Zombies! Run!
Backbone, JS, and Garbage Collection

Find a Backbone.js View if you know the Model?

Given a page that uses Backbone.js to have a Collection tied to a View (RowsView, creates a <ul>) which creates sub Views (RowView, creates <li>) for each Model in the collection, I've got an issue setting up inline editing for those models in the collection.
I created an edit() method on the RowView view that replaces the li contents with a text box, and if the user presses tab while in that text box, I'd like to trigger the edit() method of the next View in the list.
I can get the model of the next model in the collection:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
But the question is, how to find the View that is attached to that Model. The parent RowsView View doesn't keep a reference to all the children Views; it's render() method is just:
this.$el.html(''); // Clear
this.model.each(function (model) {
this.$el.append(new RowView({ model:model} ).render().el);
}, this);
Do I need to rewrite it to keep a separate array of pointers to all the RowViews it has under it? Or is there a clever way to find the View that's got a known Model attached to it?
Here's a jsFiddle of the whole problem: http://jsfiddle.net/midnightlightning/G4NeJ/
It is not elegant to store a reference to the View in your model, however you could link a View with a Model with events, do this:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
nextModel.trigger('prepareEdit');
In RowView listen to the event prepareEdit and in that listener call edit(), something like this:
this.model.on('prepareEdit', this.edit);
I'd say that your RowsView should keep track of its component RowViews. The individual RowViews really are parts of the RowsView and it makes sense that a view should keep track of its parts.
So, your RowsView would have a render method sort of like this:
render: function() {
this.child_views = this.collection.map(function(m) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
return v;
}, this);
return this;
}
Then you just need a way to convert a Tab to an index in this.child_views.
One way is to use events, Backbone views have Backbone.Events mixed in so views can trigger events on themselves and other things can listen to those events. In your RowView you could have this:
events: {
'keydown input': 'tab_next'
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
this.trigger('tab-next', this);
return false;
}
and your RowsView would v.on('tab-next', this.edit_next); in the this.collection.map and you could have an edit_next sort like this:
edit_next: function(v) {
var i = this.collection.indexOf(v.model) + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode(); // This method enables the <input>
}
Demo: http://jsfiddle.net/ambiguous/WeCRW/
A variant on this would be to add a reference to the RowsView to the RowViews and then tab_next could directly call this.parent_view.edit_next().
Another option is to put the keydown handler inside RowsView. This adds a bit of coupling between the RowView and RowsView but that's probably not a big problem in this case but it is a bit uglier than the event solution:
var RowsView = Backbone.View.extend({
//...
events: {
'keydown input': 'tab_next'
},
render: function() {
this.child_views = this.collection.map(function(m, i) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
v.$el.data('model-index', i); // You could look at the siblings instead...
return v;
}, this);
return this;
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
var i = $(e.target).closest('li').data('model-index') + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode();
return false;
}
});
Demo: http://jsfiddle.net/ambiguous/ZnxZv/

Resources