Looping through elements with the same class in Backbone View with .each? - javascript

I'm using Backbone and JQuery and would like to create a Backbone View to create equal height columns as described in method 2 of this link. The HTML looks like this:
<div class="container">
<div id="leftcolumn" class="set_equal_height"> … Lots Of Content … </div>
<div id="middlecolumn" class="set_equal_height"> … Lots Of Content … </div>
<div id="rightcolumn" class="set_equal_height"> … Lots Of Content … </div>
</div>
I put together the following Backbone View but it's not working presumably because the loop using .each is not working (the page loads with no errors, but the height of the columns is not modified by the javascript):
define([
'jquery',
'underscore',
'backbone',
], function($, _, Backbone) {
var SetEqualHeight = Backbone.View.extend({
initialize: function() {
var tallestcolumn = 0;
console.log(this.$el.height());
console.log(this.$el.attr("id"));
this.$el.each(function() {
currentHeight = $(this).height();
console.log(currentHeight);
if(currentHeight > tallestcolumn) {
tallestcolumn = currentHeight;
}
});
this.$el.height(tallestcolumn);
},
});
return SetEqualHeight;
});
I'm defining the "el" argument in a separate Wire specification as follows: { el: 'div.set_equal_height' }, and it is passing correctly since the "console.log(this.$el.height())" and console.log(this.$el.attr("id")) in the code above print out correctly. However the console.log inside the ".each" statement doesn't print out, showing that there is a problem with the ".each". I looked at this question and tried out the Underscore "_.each" method but could not figure out how to iterate through elements with a given class (i.e. div.set_equal_height) instead of a given array. Could anyone enlighten me about making .each work in the above Backbone View? Please!!!
Just for reference, the following function works by itself (I'm looking to incorporate it as a Backbone view or view helper):
function setEqualHeight(columns) {
var tallestcolumn = 0;
columns.each(function() {
currentHeight = $(this).height();
if(currentHeight > tallestcolumn) {
tallestcolumn = currentHeight;
}
});
columns.height(tallestcolumn);
}
$(document).ready(function() {
setEqualHeight($("div.set_equal_height"));
});

I assume you are instantiating your view with something like this?
new SetEqualHeight({el: '.container')
If so, that means that $el will be set to the container div inside your view. $(anything).each will iterate though all members of "anything" ... but in your case there is only one member ('.container').
The solution is to change this line:
this.$el.each(function() {
to:
this.$el.find('.set_equal_height').each(function() {
or better yet (more Backbone-y):
this.$('.set_equal_height').each(function() {
Hope that helps.

Related

Marionette.js - can I detect onAppend?

I have a silly problem, where my only solution is a sloppy hack that is now giving me other problems.
See my fiddle,
or read the code here:
HTML:
<input id='1' value='input1' />
<template id='template1'>
<input id='2' value='input2' />
</template>
JS - Item View Declaration:
// Declare an ItemView, a simple input template.
var Input2 = Marionette.ItemView.extend({
template: '#template1',
onRender: function () {
console.log('hi');
},
ui: { input2: '#2' },
onRender: function () {
var self = this;
// Despite not being in the DOM yet, you can reference
// the input, through the 'this' command, as the
// input is a logical child of the ItemView.
this.ui.input2.val('this works');
// However, you can not call focus(), as it
// must be part of the DOM.
this.ui.input2.focus();
// So, I have had to resort to this hack, which
// TOTALLY SUCKS.
setTimeout(function(){
self.ui.input2.focus();
self.ui.input2.val('Now it focused. Dammit');
}, 1000)
},
})
JS - Controller
// To start, we focus input 1. This works.
$('#1').focus();
// Now, we make input 2.
var input2 = new Input2();
// Now we 1. render, (2. onRender is called), 3. append it to the DOM.
$(document.body).append(input2.render().el);
As one can see above, my problem is that I can not make a View call focus on itself after it is rendered (onRender), as it has not yet been appended to the DOM. As far as I know, there is no other event called such as onAppend, that would let me detect when it has actually been appended to the DOM.
I don't want to call focus from outside of the ItemView. It has to be done from within for my purposes.
Any bright ideas?
UPDATE
Turns out that onShow() is called on all DOM appends in Marionette.js, be it CollectionView, CompositeView or Region, and it isn't in the documentation!
Thanks a million, lukaszfiszer.
The solution is to render your ItemView inside a Marionette.Region. This way an onShow method will be called on the view once it's inserted in the DOM.
Example:
HTML
<input id='1' value='input1' />
<div id="inputRegion"></div>
<template id='template1'>
<input id='2' value='input2' />
</template>
JS ItemView
(...)
onShow: function () {
this.ui.input2.val('this works');
this.ui.input2.focus();
},
(...)
JS Controller
$('#1').focus();
var inputRegion = new Backbone.Marionette.Region({
el: "#inputRegion"
});
var input2 = new Input2();
inputRegion.show(input2);
More information in Marionette docs: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#region-events-and-callbacks
Well, I managed to solve it by extending Marionette.js, but if anyone else has a better idea that doesn't involve extending a library, I will GLADLY accept it and buy you a doughnut.
// After studying Marionette.js' annotated source code,
// I found these three functions are the only places
// where a view is appended after rendering. Extending
// these by adding an onAppend call to the end of
// each lets me focus and do other DOM manipulation in
// the ItemView or Region, once I am certain it is in
// the DOM.
_.extend(Marionette.CollectionView.prototype, {
appendHtml: function(collectionView, itemView, index){
collectionView.$el.append(itemView.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.CompositeView.prototype, {
appendHtml: function(cv, iv, index){
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.Region.prototype, {
open: function(view){
this.$el.empty().append(view.el);
if (view.onAppend) { view.onAppend() }
},
});

Backbone view not rendering correct dom element

I have two view objects:
define(['jquery','underscore','backbone','collections/HipHopVideos','views/Video'],function($,_,Backbone){
GenreHipHop = Backbone.View.extend ({
el:"#hipHop",
collection: new HipHopVideosCollection,
initialize: function ()
{
context = this;
//fetche data for collection
this.collection.fetch({success:function ()
{
context.render ();
}
});
},
render: function ()
{
this.collection.each(function(video)
{
videoView = new VideoView({model:video});
this.$el.append(videoView.render().el);
},this);
}//end render function
});
define(['jquery','underscore','backbone','collections/PopVideos','views/Video'], function($,_,Backbone){
GenrePop = Backbone.View.extend({
el:"#pop",
collection: new PopVideosCollection(),
initialize: function ()
{
context = this;
//fetche data for collection
this.collection.fetch({success:function ()
{
context.render ();
}
});
},//end initialize function
render: function ()
{
this.collection.each(function(video)
{
videoView = new VideoView({model:video});
this.$el.append(videoView.render().el);
},this);
}//end render function
});
These Object should then append there content to this HTML:
<div class="sect">
Pop
<img class="previousBtn" src="images/nav-left.png"/>
<ul class="video_item" id="pop" page="1"></ul>
<img class="nextBtn" src="images/nav-right.png"/>
<button class="btn btn-small view-all" type="button">view all</button>
</div>
<div class="sect">
Hip Hop
<img class="previousBtn" src="images/nav-left.png"/>
<ul class="video_item" id="hipHop" page="1">
<script> //loadHipHop ()</script>
</ul>
<img class="nextBtn" src="images/nav-right.png"/>
<button class="btn btn-small view-all" type="button">view all</button>
</div>
I then call instances of both views to render to dom:
pop = new GenrePop ();
hip = new GenreHipHop();
The problem is the view elements are append to the ul tag with id #hip model and not as I outlined in the view. I don't understand what is causing this and how to fix it
It's better to control the mapping of views to DOM elements from outside the view. You don't want the view to require information about the structure of the page around it. Having the view bound to a specific element also prevents reuse in other areas of the site/page.
1 - remove the el property from your views
2 - bind the views to DOM elements like this:
var pop = new GenrePop({el: $('#pop')});
var hip = new GenreHipHop({el: $('#hipHop')});
See this article for more information on binding views to elements.
In your define statements, you are passing in 5 arguments into the first array, but only setting the first 3 to parameters to the function. For example, try changing the first line in your file to:
define(['jquery',
'underscore',
'backbone',
'collections/HipHopVideos',
'views/Video'],
function($,_,Backbone, ActHipHopVideoCollection, ActVideoView){
...
}
And then on the line where you create the collection, do this:
collection: new ActHipHopVideoCollection(),
And when you create each videoView, do this:
videoView = new ActVideoView({model:video});
That way you are using the collection and view that requirejs is getting for you and passing to this function.
You will need to make this change for the other file as well. This way, you are not using some global variables to create objects.

changing backbone views

I have a question about the way backbone handles it views.
Suppose I have the following code:
<div id="container">
<div id="header">
</div>
</div>
After this I change header into a backbone view.
How can I now remove that view from the header div again after I'm done with the view and add ANOTHER view to the same div?
I tried just overwriting the variable the view was stored in. This results in the view being changed to the new one...but it will have all the event handlers of the old one still attached to it.
Thanks in advance!
http://documentcloud.github.com/backbone/#View-setElement
This won't automatically remove the original div - you'll want to do that yourself somehow, but then by using setElement you'll have the view's element set to whatever you passed it.. and all of the events will be attached as appropriate. Then you'll need to append that element wherever it is that it needs to go.
--- Let's try this again ----
So, first thing to keep in mind is that views reference DOM elements.. they aren't super tightly bound. So, you can work directly with the jquery object under $el.
var containerView = new ContainerView();
var headerView = new HeaderView();
var anotherHeaderView = new AnotherHeaderView();
containerView.$el.append(headerView.$el);
containerView.$el.append(anotherHeaderView.$el);
anotherHeaderView.$el.detach();
containerView.$el.prepend(anotherHeaderView.$el);
Or you can create methods to control this for you.
var ContainerView = Backbone.View.extend({
addView: function (view) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.$el.append(el);
}
});
Maybe setting the views by view order?
var ContainerView = Backbone.View.extend({
initialize: function () {
this.types = {};
},
addView: function (view, type) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.types[type] = el;
this.resetViews();
},
removeView: function (type) {
delete this.types[type];
this.resetViews();
},
resetViews: function () {
this.$el.children().detach();
_.each(['main_header', 'sub_header', 'sub_sub_header'], function (typekey) {
if(this.types[typekey]) {
this.$el.append(this.types[typekey]);
}
}, this);
}
});

Can't pass data from Sammy to knockout template

I have been looking at this for quite few hours and I don't think I am able to see the solution.
This is my router.js:
define('router', ['jquery', 'config', 'nav','store'], function ($, config, nav, store) {
var
concepTouch = Sammy('body', function () {
// This says to sammy to use the title plugin
this.use(Sammy.Title);
this.use(Sammy.Mustache);
// Sets the global title prefix
this.setTitle(config.title.prefix);
// So I can access sammy inside private methods
var sammy = this;
function establishRoutes() {
// Defines the main container for content then
var mainConainer = $(config.mainContentContainerId);
// Adds animation loading class to the main container
mainConainer.addClass(config.loadingAnimationCssClass);
// iterates through routes defined in config class then
_.forEach(config.appRoutes, function(obj, key) {
// defines each one as a route
sammy.get(obj.hashV, function(context) {
// Store the requested route as the last viewed route
store.save(config.stateKeys.lastView, context.path);
// Fetches its html template
context.render(obj.tmpltURL, { 'routeData': context.params })
// Appends that htmlo template to the main container and removes loading animation
.then(function(content) {
mainConainer.removeClass(config.loadingAnimationCssClass).html(content);
});
// Finally adds the route title to the prefix
this.title(obj.title);
});
// Overriding sammy's 404
sammy.notFound = function () {
// toast an error about the missing command
toastr.error(sammy.getLocation() + ' Does not exist yet!');
// Go to last visited anf if not
sammy.setLocation(
store.fetch(config.stateKeys.lastView) || config.getDefaultRoute()
);
};
});
}
// Calls for routes to be established
establishRoutes();
}),
// runs concep touch as a sammy App with the initial view of default route
init = function () {
// Try to get today's last visit and if not available then fallback on default
concepTouch.run(store.fetch(config.stateKeys.lastView) || config.getDefaultRoute());
// Make the correct nav item active and add Click handlers for navigation menu
nav.setStartupActiveClass(store.fetch(config.stateKeys.lastView) || sammy.getLocation())
.addActiveClassEventHandlers();
};
return {
init: init,
concepTouch: concepTouch
};
});
This when I submit the search form gets this template for me:
<div id="contacts" class="view animated fadeInLeft">
<h3>Search results for {{routeData}}</h3>
<ul data-bind="template: { name: 'searchresults-template', foreach: searchResults }"></ul>
</div>
<script type="text/html" id="searchresults-template">
<li data-bind="text: type"></li>
</script>
<script>
require(['searchresults'], function (searchresults) {
searchresults.get(to Some how Get routeData.term);
});
</script>
and I can not find the right way to make Mustache pass the data from this line of router.js context.render(obj.tmpltURL, { 'routeData': context.params }) to the {{routeData.term}} inside the template.
{{routeData}} on its own returns `SAMMY.OBJECT: {"TERM": MY SEARCH TERM}`
which I can't navigate to the property i want to from it using . notation. Furthermore even if that worked it can not be passed into Javascript which is what I really need as
searchresults.init(); is waiting for this paramter `searchresults.init(routeData.term);`
Or maybe the answer is to find a way to access sammy's context here? outside of sammy in order to get the params? something like Sammy.Application.context.params['term'] but ofcourse application has no such method so don't know!? :(
Am I going totally the wrong way about it? How Can I easily pass the query string params as accessible objects inside my template so knockout can use it.
Your help is greatly appreciated.
<div id="contacts" class="view animated fadeInLeft">
<h3>Search results for {{#routeData}}{{term}}{{/routeData}}</h3>
<ul data-bind="template: { name: 'searchresults-template', foreach: searchResults }"></ul>
</div>
<script type="text/html" id="searchresults-template">
<li data-bind="text: type"></li>
</script>
<script>
require(['searchresults'], function (searchresults) {
var searchTerm = "{{#routeData}}{{term}}{{/routeData}}";
searchresults.get(searchTerm);
});
</script>

Backbone.js backed list not being refreshed by jQuery mobile (listview(‘refresh’))

I’m trying to add sort options to a JQM list which is backed by a backbone.js collection. I’m able to sort the collection (through the collection’s view) and rerender the list, but JQM isn’t refreshing the list.
I’ve been searching and I found several questions similar to mine (problems getting the JQM listview to refresh) but I’ve been unable to get it to work.
I’ve tried calling $(‘#list’).listview(‘refresh’) and $(‘#list-page’).page() etc. to no avail. I suspect that Perhaps I’m calling the refresh method in the wrong place (to early), but I’m not sure where else I should put it (I’m just starting out with backbone).
Here’s the markup and js.
HTML:
<div data-role="page" id="Main">
<div data-role="header"><h1>Main Page</h1></div>
<div data-role="content">
<ul data-role="listview">
<li>Page 1</li>
</ul>
</div>
<div data-role="footer"><h4>Footer</h4></div>
</div>
<div data-role="page" id="Page1">
<div data-role="header">
Back
<h1>Items</h1><a href="#dvItemSort" >Sort</a></div>
<div data-role="content">
<div id="dvTest">
<ul id="ItemList" data-role="listview" data-filter="true"></ul>
</div>
</div><div data-role="footer"><h4>Footer</h4></div></div>
<div data-role="page" id="dvItemSort">
<div data-role="header"><h4>Sort</h4></div>
<a href="#Page1" type="button"
name="btnSortByID" id="btnSortByID">ID</a>
<a href="#Page1" type="button"
name="btnSortByName" id="btnSortByName">Name </a>
</div>
Javascript:
$(function () {
window.Item = Backbone.Model.extend({
ID: null,
Name: null
});
window.ItemList = Backbone.Collection.extend({
model: Item
});
window.items = new ItemList;
window.ItemView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
this.model.bind('change', this.render, this);
},
render: function () {
$(this.el).html('<a>' + this.model.get('Name') + '</a>');
return this;
}
});
window.ItemListView = Backbone.View.extend({
el: $('body'),
_ItemViews: {},
events: {
"click #btnSortByID": "sortByID",
"click #btnSortByName": "sortByName"
},
initialize: function () {
items.bind('add', this.add, this);
items.bind('reset', this.render, this);
},
render: function () {
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
// $('#ItemList').listview();
// $('#Page1').trigger('create');
// $('#Page1').page(); //also doesn't work
},
add: function (item) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el);
},
sortByName: function () {
items.comparator = function (item) { return item.get('Name'); };
items.sort();
},
sortByID: function () {
items.comparator = function (item) { return item.get('ID'); };
items.sort();
}
});
window.itemListView = new ItemListView;
window.AppView = Backbone.View.extend({
el: $('body'),
initialize: function () {
items.add([{ID: 1, Name: 'Foo 1'}, {ID:2, Name: 'Bar 2'}]);
},
});
window.App = new AppView;
});
EDIT: I realized that the first line of html markup I posted wasn't displaying in my post so I pushed it down a line.
EDIT 2: Here's a link to a jsfiddle of the code http://jsfiddle.net/8vtyr/2/
EDIT 3 Looking at the resulting markup, it seems like JQM adds some of the classes to the list items. I tried adding them manually using a flag to determine whether the list was being reRendered as a result of a sort and the list then displays correctly.
However, besides being somewhat of an ugly solution, more importantly my backbone events on the “item” view no longer fire (in the code example I posted I didn’t put the code for the events because I was trying to keep it as relevant as possible).
EDIT 4 I sort of got it working by clearing my cache of views and recreating them. I posted my answer below.
EDIT 5
I updated my answer with what i think is a better answer.
I'm not sure if this should be its own answer or not (i did look through the FAQ a bit), so for now I’m just updating my previous answer.
I have now found a better way to sort the list using my cached views. Essentially the trick is to sort the collection, detach the elements from the DOM and then reattach them.
So
The code now would be
$list = $('#ItemList')
$('li', $list ).detach();
var frag = document.createDocumentFragment();
var view;
_.each(item.models, function (mdl) {
view = this._ItemViews[item.get('ID')];
frag.appendChild(view.el);
},this);
$list.append(frag);
OLD ANSWER
I sort of solved the problem. I was examing the rendered elements and I noticed that when the elements were “rerendered” (after the sort) they lost the event handlers (I checked in firebug). So I decided to clear my cache of views and recreate them. This seems to do the trick, though I’m not really sure why exactly.
For the code:
Instead of:
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
I clear the view cache and recreate the views.
$('#ItemList').empty();
this._ItemViews = {};
_.each(items.models, function (item, idx) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el)
}, this);
$('#ItemList').listview('refresh'); //works now
I think it would probably be better if I didn’t need to regenerate the cache, but at least this is a working solution and if I don't get a better answer then I'll just accept this one.
I had some luck in solving this, but the reason remains obscure to me.
Basically, at the top of my render view after establishing the html() of my element, I call listview(). Then, any further items I might add to a list call listview('refresh').

Categories