Saturday, January 26, 2013

Managing Events Between Multiple Backbone Views

At first, working with Backbone's library of objects and their associated events might take a little time to completely understand. Events are built-in to every object and provide both publish and subscribe capabilities. The real strength of this architecture is visible in the Model objects where data becomes the driver for generating events. With that capability, it becomes very easy to build views that are driven from data-related changes:


var myView = Backbone.View.extend({

initialize: function () {
this.listenTo( this.model, 'change', this.doSomthing );
},

doSomething: function () {
...
}
});

...

myModel.set( 'someData', 'someValue' );

...



Any view listening to the same model will be notified of changes, etc and can take action. Now, what about non-data related events? There's already built-in support for binding to DOM events that occur inside the view's top-level DOM element:


var ItemView = Backbone.View.extend({

...

events: {
"click a": "handleItemClick"
},

...

handleItemClick: function () {
...
}

});


Additionally, views can bind and trigger their own custom events (ie onRender is a pretty common extension). And navigation can be handled using a router:


var Workspace = Backbone.Router.extend({

routes: {
"list/:type": "list",
"detail/:id": "detail",
}

});

function handleMenuSelect( type ) {

// Coordinate other views/models based on event

}

function handleItemClick( id ) {

// Coordinate other views/models based on event

}


$(function() {
var router = new Workspace();

router.on( 'route:list', handleMenuSelect );
router.on( 'route:detail', handleItemClick );

Backbone.history.start();
});


Between those different types of handlers, you can cover almost of the cases you'll encounter while building an application. But what about events that might affect other views? Certain actions that occur on the page or in application logic might need to be handled by other views other than the view originating the action (or working with that part of the DOM). One example that I recently encountered was a page where I had several menu/select type widgets which would display a popup box to make a choice. The widgets were spread across several views and at no point in time did I want more than one of the popup menus to be visible at the same time. What I needed was a way to communicate that a menu was being displayed and have all the other views ensure their menus were hidden. After trying several approaches and reviewing what others have done, I've identified three alternative ways to handle the problem. Each has their pros and cons but all of them maintain the separation of roles of each view.

Before I start outlining the alternatives, the example I'm using is a simple navigation/data table setup. The navigation could be a menu and each row in the table can be selected which could trigger some other action. I know this could just be managed using a router to implement the required handling. However, for purposes of illustration, I'm going to handle it through a series of event handlers. There are generally many ways to solve the same problem so having several of them in your bag of tricks can't hurt. I chose this example since its a common pattern and easy to visualize.

Option 1: Listen and Bubble



Every Backbone View already has the ability to trigger and bind to events. It would seem only natural to just mixin the Backbone.Events object with a mediator type object and have it listen to any events the views trigger:


var App = function () {

}

$.extend(App.prototype, Backbone.Events, {

start: function () {

...

this.nav = new NavView();
$('#nav').html( this.nav.render().$el );

this.table = new TableView({ collection: this.tableData });
$('#table').html( this.table.render().$el );

this.listenTo( this.nav, 'menu:select', this.handleMenuSelect );
this.listenTo( this.table, 'item:click', this.handleItemClick );

...
},

handleItemClick: function ( data ) {

// Coordinate other views/models based on event

}
});


In this example, App is my main mediating object that will coordinate all the views. It creates the navigation and table views and will be able to respond to the events they generate. However, the table view will be creating many item views for each row in the table. The App object won't be able to listen to any events generated by those views since it didn't create them. The only way to get the item views event to the App object will be via the table view. So as the table creates the item views, it will need to listen to the "item:click" event as well:


var TableView = Backbone.View.extend({

updateTable: function () {

var that = this,
ref = this.collection,
$table;

_.invoke(this._itemRowViews, 'remove');

$table = this.$('tbody');

this._itemRowViews = this.collection.map(
function ( obj ) {
var v = new ItemView({ model: ref.get(obj) });

$table.append(v.render().$el);
that.listenTo( v, 'item:click', that.handleItemClick );

return v;
});


},

handleItemClick: function ( data ) {

// Handle locally
...

// Bubble up the chain
this.trigger('item:click', data);
}
});

var ItemView = Backbone.View.extend({

...

events: {
"click a": "handleItemClick"
},

...

handleItemClick: function () {

// Handle locally
...

// Bubble up the chain
this.trigger('item:click', this.model);
}

});


Here, you can see that when the item view triggers the "item:click" event, the table view captures it, does any processing and then triggers the same event - effectively bubbling it up the chain in the view hierarchy. If you had a very deeply nested set of views, getting events from the lowest view up to the app would require a lot of catch and bubble work. If all the views in the chain need to handle it, then it might make sense. If the only goal is to get the event up to the App object, then it becomes fairly tedious.

Option 2: Create a Common Event Object



The next option is to flip the problem over and create a common event object that every view gets a reference to and can trigger events on so the App object (or any view, really) can respond. Now, instead of wiring up each view to listen to the next view, you just need to pass the object as an option to each view created so it can call trigger() on that object:


var App = function () {

}

$.extend(App.prototype, {

start: function () {

...

this.ev = $.extend({}, Backbone.Events);

this.nav = new NavView({ ev: this.ev });
$('#nav').html( this.nav.render().$el );

this.table = new TableView({ collection: this.tableData, ev: this.ev });
$('#table').html( this.table.render().$el );

this.ev.on( 'menu:select', this.handleMenuSelect, this );
this.ev.on( 'item:click', this.handleItemClick, this );

...
},

handleItemClick: function ( data ) {

// Coordinate other views/models based on event

}
});


var TableView = Backbone.View.extend({

ev: null,

initialize: function ( options ) {
this.ev = options.ev;
},

updateTable: function () {

var that = this,
ref = this.collection,
$table;

_.invoke(this._itemRowViews, 'remove');

$table = this.$('tbody');

this._itemRowViews = this.collection.map(
function ( obj ) {
var v = new ItemView({ model: ref.get(obj), ev: that.ev });

$table.append(v.render().$el);

return v;
});


}

});

var ItemView = Backbone.View.extend({

...

ev: null,

initialize: function ( options ) {
this.ev = options.ev;
},

events: {
"click a": "handleItemClick"
},

...

handleItemClick: function () {

// Handle locally
...

// Notify everyone listening...
this.ev.trigger('item:click', this.model);
}

});


Notice how ev is created in App and then passed to each view. App then binds to the event and only the view that triggers it needs to do anything. All the views in between just need to keep passing the object to any views they create. This is the approach that Marionette takes in the Application object available in that library. The only issue is that you must always pass the reference along to each view. However, since it is a common object, any view can listen to events triggered by another view. This is similar to how you would listen to a model's change event from any view referencing the model and respond to it. In a way, this is like a model but without any data - just the event triggers.

Option 3: Use a Global Object



If you don't want to pass an object reference around, you can always use a global object to provide a common event channel. In fact, Backbone sets one up for you in the library:



// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
_.extend(Backbone, Events);



So, instead of ev being created and passed around, you can just reference Backbone:


var App = function () {

}

$.extend(App.prototype, {

start: function () {

...

this.nav = new NavView({ ev: this.ev });
$('#nav').html( this.nav.render().$el );

this.table = new TableView({ collection: this.tableData, ev: this.ev });
$('#table').html( this.table.render().$el );

Backbone.on( 'menu:select', this.handleMenuSelect, this );
Backbone.on( 'item:click', this.handleItemClick, this );

...
},

handleItemClick: function ( data ) {

// Coordinate other views/models based on event

}
});


var TableView = Backbone.View.extend({

updateTable: function () {

var that = this,
ref = this.collection,
$table;

_.invoke(this._itemRowViews, 'remove');

$table = this.$('tbody');

this._itemRowViews = this.collection.map(
function ( obj ) {
var v = new ItemView({ model: ref.get(obj) });

$table.append(v.render().$el);

return v;
});


}

});

var ItemView = Backbone.View.extend({

...

events: {
"click a": "handleItemClick"
},

...

handleItemClick: function () {

// Handle locally
...

// Notify everyone listening...
Backbone.trigger('item:click', this.model);
}

});


If you don't need to compartmentalize your events, the global object is very easy to use. No extra work required in the views - just bind to an event and trigger it where needed. In an app where everything on the page might need to deal with an event, this is a straight-forward way to manage it.

In the end, how you handle non-data related events that are not local to the views (and not handled by your router) is really based on the problem your trying to solve. For problems like my open menu issue, the global object made the most sense. For other problems, one of the other solutions might be a better approach. I don't think any one option is superior to the other - just certain circumstances lend themselves better to a given approach.

Monday, January 21, 2013

Adding Labels/Tick Marks to the jQuery UI Slider Widget

jquery-ui-labeled-sliderThe base jQuery UI Slider really seems to need some labels to provide the user some visual reference to where certain values exist along the slider bar. I needed this reference on a recent project so set out to find a way to add them. My original intent was just to wrap the slider with some static HTML since I already knew the range of values I needed. I found this gist and used it as a guide and augmented to meet my needs. The actual setup is not all that complicated. If you look at the required markup:


<div class="ui-slider-wrapper ui-widget horizontal">

<div class="ui-slider-labels">
<div class="ui-slider-label-ticks" style="left: 0%;"><span></span><br>0</div>
<div class="ui-slider-label-ticks" style="left: 17%;"><span></span><br>1</div>
<div class="ui-slider-label-ticks" style="left: 33%;"><span></span><br>2</div>
<div class="ui-slider-label-ticks" style="left: 50%;"><span></span><br>3</div>
<div class="ui-slider-label-ticks" style="left: 67%;"><span></span><br>4</div>
<div class="ui-slider-label-ticks" style="left: 83%;"><span></span><br>5</div>
<div class="ui-slider-label-ticks" style="left: 100%;"><span></span><br>6</div>
</div>

<div id="slider1" class="ui-slider ui-slider-horizontal ui-widget-content ui-corner-all">
<a href="#" class="ui-slider-handle ui-state-default ui-corner-all" style="left: 0%;"></a>
</div>

</div>


The actual slider widget is id="slider1" and its wrapped with a DIV containing labels inserted before it. The labels are styled so that the positioning matches how the slider widget positions the handle (note that the handle is at left: 0% - as it is moved it will progress through the same set of percentages that the labels are set at).

With that basic idea in mind, its time to extend the Slider widget to add the required content. Augmenting Slider with these labels is made easier since we're not trying to affect its functionality - just add some visual cues to help the user know where to position the slider handle. As such, we only need to add some code to the _create, _setOption, and _destroy methods.

To start, we need to call $.widget() to create our new enhanced widget from the existing Slider:



$.widget( "ui.labeledslider", $.ui.slider, {

options: {
tickInterval: 0,
tickLabels: null
},

uiSlider: null,
tickInterval: 0,



In this case, I'm calling it LabeledSlider. I added two options that can be set to control the rendering of the labels. tickInterval can be set to a number greater than the current step option. This means the handle will stop at points in between labeled points on the slider. It can be useful if you have a large range with a small step and don't want a label at every point along the way. The tickLabels provides a way to label the ticks with something other than the actual value of the slider at a given point. It can be an array or hash indexed by number. If a label exists, it will use it, otherwise, it will just use the numeric value at that point.

tickInterval is repeated as an object variable outside of the options because the tickInterval must be greater or equal to the slider's step option. The following code is run to ensure this relationship is maintained:


_alignWithStep: function () {
if ( this.options.tickInterval < this.options.step )
this.tickInterval = this.options.step;
else
this.tickInterval = this.options.tickInterval;
},


uiSlider will be used to cache a reference to the wrapper element which will serve as the actual widget for this version of slider. this.element will continue to point to the actual Slider element but since the labels are going to be siblings of the slider, its easier to have the parent of both handy throughout the code. Also, when cleaning up and responding to the widget(), it will useful:


_destroy: function() {
this._super();
this.uiSlider.replaceWith( this.element );
},

widget: function() {
return this.uiSlider;
}


Ok, now that some of the housekeeping is out of the way, we can actually wrap the slider, add the label container, and create all the individual labels. This needs to be split up sightly since during the life-cycle of our widget, the options can be changed that might affect the rendered labels. However, the wrapper and label container can be created once so they can be placed in the _create() function:


_create: function( ) {

this._super();

this.uiSlider =
this.element
.removeClass( 'ui-widget' )
.wrap( '<div class="ui-slider-wrapper ui-widget"></div>' )
.before( '<div class="ui-slider-labels">' )
.parent()
.addClass( this.orientation )
.css( 'font-size', this.element.css('font-size') );

this._alignWithStep();

if ( this.orientation == 'horizontal' ) {
this.uiSlider
.width( this.element.width() );
} else {
this.uiSlider
.height( this.element.height() );
}

this._drawLabels();
},



Since we're overriding the slider's implementation of _create(), we have to call _super() to execute the original _create() function defined in the Slider widget. Once that is complete, we can add our features. In this case, the element created by Slider is wrapped and the label container is added before the slider element.

At this point, before we can render the actual labels, we need to address several issues related to the wrapping the original slider element. There were two problems I encountered when attempting to match the style of the label container to the slider element. First, since the slider element might be the target of certain styles that affect the size of the slider, those styles may not affect the wrapper element. The two styles that I found that were particularly problematic are height/width and font size. The slider is sized relative to the current font size (ie the height of the slider bar is 0.8em). If you were to directly set the font size on the slider element (ie 8pt) and the normal font size for the page is 12pt, the slider and label container would not size correctly relative to each other. Maintaining the same font size becomes important if everything is going to line up. I didn't want to just say that you can't directly set the font on the slider. So to work around it, I just copied the style from the slider element to the wrapper so everything is matched up properly:



this.uiSlider.css( 'font-size', this.element.css('font-size') )



Next, the height/width needs to be the same between the slider and the wrapper. I set the style on the label container to be 100% of the parent so as long as the wrapper matches, the labels will too. Again, to work around any direct styling, I just copy the size to the wrapper:



if ( this.orientation == 'horizontal' ) {
this.uiSlider
.width( this.element.width() );
} else {
this.uiSlider
.height( this.element.height() );
}


Of course, if you start dynamically changing these styles after the widget is created, strange things might happen depending on where the targets for the styles are set. This scenario is probably not too common so I'm not going to worry about it at this time.

With that problem addressed, the labels and tick marks can finally be rendered:


_drawLabels: function () {

var labels = this.options.tickLabels || {},
$lbl = this.uiSlider.children( '.ui-slider-labels' ),
dir = this.orientation == 'horizontal' ? 'left' : 'bottom',
min = this.options.min,
max = this.options.max,
inr = this.tickInterval,
cnt = ( max - min ) / inr,
i = 0;

$lbl.html('');

for (;i<=cnt;i++) {
$('<div>').addClass( 'ui-slider-label-ticks' )
.css( dir, (Math.round( i / cnt * 10000 ) / 100) + '%' )
.html( ( dir == 'left' ? '<span></span><br/>' : '<span></span> ' ) + ( labels[i*inr+min] ? labels[i*inr+min] : i*inr+min ) )
.appendTo( $lbl );
}

},


Nothing exceptionally fancy here - just create the DIV, position it, and add the content. The most complicated part is calculating the correct scale for the positions and which labels to display. Also note that the orientation needs to be honored here too and different content added and positioning CSS (left vs bottom).

One final step that needs to be taken care of is responding to changes to the options. This functionality made it necessary to split the drawLabels() from the main creation function. When any option changes that could affect the labels, they need to be redrawn:


_setOption: function( key, value ) {

this._super( key, value );

switch ( key ) {

case 'tickInterval':
case 'tickLabels':
case 'min':
case 'max':
case 'step':

this._alignWithStep();
this._drawLabels();
break;

case 'orientation':

this.element
.removeClass( 'horizontal vertical' )
.addClass( this.orientation );

this._drawLabels();
break;
}
},



Here, all the different slider options plus the ones defined in the new widget cause a redraw. The only options we don't care about are range and value since they don't affect the labels and tick content/positioning.

A demo is on my sandbox. If you want to use the widget, you'll need the both the JS and CSS source files. The jQuery UI library may not have every feature you ever wanted, however, it does make it pretty easy to extend and tweak to meet your needs. Since I will probably use this quite often, I took the time to package it up and add to my extensions library on GitHub.

Saturday, January 19, 2013

Creating Sortable Tables with Backbone Collections

Probably the most common use cases found in any application is sorting a table of data. There are so many ways you can handle this scenario. Backbone offers one more approach you can consider when choosing a solution. Collections have built in support for establishing a sort order, maintaining that order as models are added/removed, and triggering events when a sort operation occurs. Given that, it seemed like an interesting feature to try out when building my little prototype report.

Building off the example I started in my previous post, I already have my basic model, collection, and two views which render the table of data. Now I just need to extend both the movie collection and movie table view to enable sorting any of the columns in either ascending or descending order.

Collections may have built-in sorting, but they do not have the ability to externally change which column to sort on or the direction of the sort. The comparator function does provide a lot of flexibility for returning what or how to sort. One variation allows passing two models as arguments and ordering those models based on the return value:


comparator: function(a, b) {
var a = a.get('title'),
b = b.get('title');

if (a == b) return 0;

return a > b ? 1 : -1;
}


A return value of -1 means the first model comes before the second model. A return value of 1 means the opposite. Returning 0 means they are equal. If you've used PHP, this should look familiar since its the same scheme used for the callback to usort.

Of course, if you just wanted to sort "title" in ascending order, you could just write:


comparator: function(m) {
return m.get('title');
}


However, I want to be able to sort on any column in either ascending or descending order. To do this, I need to extend Collection a little bit to add the attribute to sort on and the direction:


var Movies = Backbone.Collection.extend({

model: Movie,

sortAttribute: "rank",
sortDirection: 1,

sortMovies: function (attr) {
this.sortAttribute = attr;
this.sort();
},

comparator: function(a, b) {
var a = a.get(this.sortAttribute),
b = b.get(this.sortAttribute);

if (a == b) return 0;

if (this.sortDirection == 1) {
return a > b ? 1 : -1;
} else {
return a < b ? 1 : -1;
}
}

});


Now I can call sortMovies to reorder the collection of data based on those attributes:


myMovies.sortDirection = -1;
myMovies.sortMovies('gross');


You might ask why I make the direction 1 and -1 versus something more obvious like 'ASC'/'DSC'. Well, call me lazy, but 1/-1 are pretty easy to toggle between using:


myMovies.sortDirection *= -1;


That will come in handy in the view when I want to toggle the order order on a column between ascending and descending order.

With the collection functionality in place, I can now focus on adding functionality to my view that will enable clicking a header to sort the column and adding visual cues about which column is currently sorted and in which direction:


var MovieTable = Backbone.View.extend({

_movieRowViews: [],

tagName: 'table',
template: null,

// Make it easier to change later
sortUpIcon: 'ui-icon-triangle-1-n',
sortDnIcon: 'ui-icon-triangle-1-s',

// Need to respond to clicks on the table headers
events: {
"click th": "headerClick"
},

// Make to listen to sort events - the clicks on the header
// will change the sort order but the sort event will trigger
// rendering the data after the sort is complete.
initialize: function() {

this.template = _.template( $('#movie-table').html() );
this.listenTo(this.collection, "sort", this.updateTable);
},

// One time setup of the column headers to add a span
// to hold the icon indicating the current sort state
render: function() {

this.$el.html(this.template());

// Setup the sort indicators
this.$('th div')
.append($('<span>'))
.closest('thead')
.find('span')
.addClass('ui-icon icon-none')
.end()
.find('[column="'+this.collection.sortAttribute+'"] span')
.removeClass('icon-none').addClass(this.sortUpIcon);

this.updateTable();

return this;
},

// Now the part that actually changes the sort order
headerClick: function( e ) {
var $el = $(e.currentTarget),
ns = $el.attr('column'),
cs = this.collection.sortAttribute;

// Toggle sort if the current column is sorted
if (ns == cs) {
this.collection.sortDirection *= -1;
} else {
this.collection.sortDirection = 1;
}

// Adjust the indicators. Reset everything to hide the indicator
$el.closest('thead').find('span').attr('class', 'ui-icon icon-none');

// Now show the correct icon on the correct column
if (this.collection.sortDirection == 1) {
$el.find('span').removeClass('icon-none').addClass(this.sortUpIcon);
} else {
$el.find('span').removeClass('icon-none').addClass(this.sortDnIcon);
}

// Now sort the collection
this.collection.sortMovies(ns);
},

// This code has not changed from the example setup in the previous post.
updateTable: function () {

var ref = this.collection,
$table;

_.invoke(this._movieRowViews, 'remove');

$table = this.$('tbody');

this._movieRowViews = this.collection.map(
function ( obj ) {
var v = new MovieRow({ model: ref.get(obj) });

$table.append(v.render().$el);

return v;
});
}

});



Since I already have jQuery UI loaded on the page, I used those icons for the sort indicators. However, there is an annoying little quirk with those icons that needed a workaround. The ui-icon class attaches the background image, sets the size of the container (to 16x16), and makes it a block element. The only desired effect I wanted was the 16x16 placeholder it creates. All I really want is a blank space that will eventually have a visible icon if the column is sorted. The follow CSS is required to ensure the indicator doesn't wrap and the default icon is not showing when a column is not currently sorted:


<style>
#movies th { white-space: nowrap; cursor: pointer; }
#movies th span { display: inline-block; margin-left: 5px; }

.icon-none { visibility: hidden; }
</style>


The demo and full source are on my sandbox. This functionality could easily be abstracted into a set of base collections/views that handle the details of the sorting. Most likely, several people have already built this. However, I wanted to take the base Backbone functionality for a spin and see what it could do. So far, its pretty powerful. Next step - create aggregate summaries on the data and perform some basic slicing and dicing.

Sunday, January 13, 2013

Building Web Applications with Backbone - Getting Started

If you're reading this post, you, like me, have already decided to give Backbone a try. You probably looked at the Backbone reference and now are asking yourself - "This looks great! But how do I do use this to make my development easier and my application better?". I often feel this way when reading through a reference document. Its generally not organized into why use this thing here or these two things are designed to work together so use them this way. I'm not going to write a guide here, however, because I found a reasonable reference as a starting point. I recommend looking at the early release of the O'Reilly book Developing Backbone.js Applications. I don't know how long it will be there, but if you have to pay for a digital copy, it is a worthwhile read.

I will say that after the little prototype reporting application I built, I really do like the structure of the Backbone library. I found the implementation of the MVC pattern not so overly strict that I did not feel like I would have any flexibility. In fact, some will find it to be so loosely coupled that they may find it does not fully meet their needs. The real power of the library is a combination of binding events to models and leveraging Underscore to work with collections of models. The router and view components provide basic functionality to connect model events and render data to the screen.

As you start working with Backbone views, you will quickly see that you are writing some of the same basic code over and over again. Because the library makes no assumptions about how you will get data on the screen, you have to wire that all up in each view. Once you pick a methodology for what templates you will use and how you will affect changes to the DOM, each view will have a similar starting point. For larger applications, you can either build some super classes to abstract these functions away from each view or find another library that extends Backbone with these features. Marionette is a good example of a library which adds extra structure to Backbone and can reduce the amount of boilerplate code plus help organize your application (its more opinionated than Backbone on how to layout an application's components).

For my needs, I don't a lot of extra structure or assumptions about how I want to manage my data and rendering. My first goal was to just get some models setup with data and use a view to render this data to the screen. From there, I started layering on the functionality I wanted and attempted to leverage the built-in tools of Backbone when appropriate. As I worked on this project, it was apparent that since Backbone is not forcing you into any specific ways of rendering a collection of data, you can get pretty lazy and eventually find yourself refactoring chunks of your code so you are, again, actually using the features Backbone provides.

As an example, I setup a sample dataset that I wanted to render into a table. My ultimate goal with this table will be to make it sortable, add some animated graphs, and provide some interactive features to explore the data. Starting out, I found a pattern for using Underscore template partials that seemed like a good idea. Its so simple, it almost seemed like overkill to even bother with Backbone - I could just pass an array of data to the Underscore template and be done. However, this is as good a starting point as any, so here's how the code looks to make it work:



var Movie = Backbone.Model.extend({

defaults: {
title: '',
gross: 0,
rank: -1,
year: 1900,
studio: ''
}

});

var Movies = Backbone.Collection.extend({

model: Movie,

comparator: function(movie) {
return movie.get('rank');
}

});

var MovieTable = Backbone.View.extend({

tagName: 'table',
template: null,

initialize: function() {

this.template = _.template( $('#movie-table').html() );
},

render: function() {

this.$el.html(this.template({
movies: this.collection,
movieTemplate: _.template( $('#movie-row').html() )
}) );

return this;
}

});



And here's the template with the partial to render the table:



<script id="movie-table" type="text/template">

<thead>
<tr>
<th column="rank"><div>Rank</div></th>
<th column="title"><div>Title</div></th>
<th column="studio"><div>Studio</div></th>
<th column="gross"><div>Gross</div></th>
<th column="year"><div>Year</div></th>
</tr>
</thead>
<tbody>
<% movies.each(function(movie) { %>
<%= movieTemplate(movie.toJSON()) %>
<% }); %>
</tbody>

</script>

<script id="movie-row" type="text/template">
<tr>
<td><div><%= rank %></div></td>
<td><div><%= title %></div></td>
<td><div><%= studio %></div></td>
<td><div><%= Globalize.format(gross, 'C') %></div></td>
<td><div><%= year %></div></td>
</tr>
</script>


The only thing left to do is to create an instance of the view and render it into the DOM:


$(function() {

var movieList = new Movies(movieData);

var movieView = new MovieTable({ collection: movieList });

$('.wrapper').html( movieView.render().$el.attr('id', 'movies') );

});


So, really what's the problem with this implementation? Well, nothing, until you want to add actions either to the table as a whole or to individual rows in the table. Because all the rendering is done in one spot, any changes in the data or DOM will require either a lot of jQuery magic or completely rendering the whole table again. Neither of those two approaches will really take advantage of what Backbone has to offer.

Another way to render the table is to break it up into the main table view which draws the header and then a series of individual item views which render each row of the table:


var MovieTable = Backbone.View.extend({

_movieRowViews: [],

tagName: 'table',
template: null,

initialize: function() {

this.template = _.template( $('#movie-table').html() );
},

render: function() {

this.$el.html(this.template());

this.updateTable();

return this;
},

updateTable: function () {

var ref = this.collection,
$table;

_.invoke(this._movieRowViews, 'remove');

$table = this.$('tbody');

this._movieRowViews = this.collection.map(
function ( obj ) {
var v = new MovieRow({ model: ref.get(obj) });

$table.append(v.render().$el);

return v;
});
}

});

var MovieRow = Backbone.View.extend({

tagName: 'tr',
template: null,

initialize: function() {

this.template = _.template( $('#movie-row').html() );

},

render: function() {

this.$el.html( this.template( this.model.toJSON()) );

return this;
}

});



Here, the major change is to add the MovieRow view and then setup a function in the MovieTable view to iterate over the collection of models and render each row as a separate view. Now, using this scheme, the advantages of each Backbone view being bound to a specific record and being able to listen to events both on that record and the row in the DOM can later be realized as more functionality is added to the table.

You may be wondering why the line


_.invoke(this._movieRowViews, 'remove');


is in the updateTables() function. If the rows are only ever rendered once, then this line is not necessary. However, my intention is to render the rows at various times during the life-cycle of the application so I need to track all the item views I create and ensure I remove them before creating a new set of rows. If I don't do this, I risk having the dreaded "zombie" problem and memory leaks because events remain bound to variables that continue to persist.

This example doesn't do anything spectacular nor does it really utilize some of the key features of Backbone. However, it does represent a basic boilerplate pattern I would use when working with a table of data. Both the version using partials and the one using item views are on my sandbox for reference. In my next post, I will continue to flush out additional features by making the table sortable.

Sunday, January 6, 2013

Animating CSS3 3D Transforms: Performance with Many Elements in the DOM

For the New Year, I wanted to try making a firework effect in the browser. The idea I had was to create a containing DIV which represented a single firework. Within that DIV, each "spark" consists of a placeholder DIV with the actual DIV representing the spark in the top left corner. Each placeholder is then rotated around relative to the center on the Z and Y-axis. Once setup, the bursting of the firework can be controlled by resizing the placeholders. The spark would stick in the corner of the placeholder and the browser would do the work to figure out where in 3D space everything needed to exist. The approach works, however, only in certain browsers and, at least in the ones I tested, only smoothly in Chrome. However, I decided to write up what I did and use it as a reference as browser technology continues to improve.

To begin, we need some CSS to define the different components. The important part is configuring the 3D elements and define the actual individual embers of the exploding firework:



.boom {
position: absolute;
height: 300px;
width: 300px;
top: 300px;
left: 300px;

perspective: 500px;
}

.boom > div {
position: absolute;

transform-origin: 50% 50% 0px;
transform-style: preserve-3d;
}

.place {
top: 0;
left: 0;
height: 0;
width: 0;
}

.spark {
width: 4px;
height: 3px;
border-radius: 5px;
background-color: #f00;
color: #f00;
}


Here, the perspective style enables the 3D effect by establishing a vanishing point. The pereserve-3d setting ensures that same point is used by all the children. In the final animation, all the HTML is generated dynamically. Each boom element is added to the DOM with all its child elements, animated, and then removed. However, in the initial test, I created a static boom DIV and dynamically added the place and spark elements to it. Below is a snippet of parts of the resulting HTML that is generated. All of the transforms have randomized positioning to ensure the resulting sphere is not too uniform.


<div class="boom">
<div style="transform: translateX(-39px)
rotateY(0deg)
rotateZ(24deg);"
class="place">
<div style="transform: rotateY(0deg);" class="spark"></div>
</div>

<div style="transform: translateX(-40px)
rotateY(0deg)
rotateZ(47deg);"
class="place">
<div style="transform: rotateY(0deg);" class="spark"></div>
</div>

...

<div style="transform: translateX(-26px)
rotateY(20deg)
rotateZ(-6deg);"
class="place">
<div style="transform: rotateY(-20deg);" class="spark"></div>
</div>

<div style="transform: translateX(-26px)
rotateY(20deg)
rotateZ(23deg);"
class="place">
<div style="transform: rotateY(-20deg);" class="spark"></div>
</div>

</div>


Notice in the last two place elements, the spark element has a counter-rotation on the Y-axis to cancel out the rotation on the parent place element. Without this, the rotated sparks would gradually squish until they disappeared. This is because the DIV is flat and as it rotates around the Y-axis, at the 90 degree point, all you see is the edge which as no size. The counter-rotation ensures the spark is always facing out toward the screen.

Finally, the place elements have no initial height or width. These will be animated to make the explosion effect. The code to generate the HTML and set all the 3D transforms is as follows:



var $b = $('.boom');
var i,
cnt = 150,
x = 0, y = 0, z = 0,
incZ = 40,
incY = 20;


// Make a bunch of sparks to assemble the
// firework.
for (i=0;i<cnt;i++)
{
// Rotate each spark container (.place) on the
// Z and Y axis. Everything needs a little
// random staggering to avoid looking too uniform

z += Math.floor(incZ + Math.random() * 10 - 20);
if ( z > 360 )
{
y += incY;
z = Math.floor(Math.random() * 5 - 10);

if ( y > 360 )
y = 0;
}

// Shifting a little on the X-axis helps the
// staggering effect even more.
x = Math.floor(Math.random() * 20 - 40);

// Now create the actual spark. It will be contained by
// the .place wrapper which will be what is actual resized and moved
// around during the animation.

// The spark does need to be rotated in the negative Y direction
// to compensate for the Y rotation on the container, otherwise,
// it will appear like a line in some cases since it will be
// rotated on its side.

$('<div class="place"><div class="spark"></div></div>')
.appendTo($b)
.css('transform', 'translateX('+x+'px) rotateY('+y+'deg) rotateZ('+z+'deg)')
.find('.spark')
.css('transform', 'rotateY(-'+y+'deg)');
}



Most of this code is spent creating the randomized positions. The final part of the loop actually creates the elements, sets their classes, and initializes their 3D location.

The only remaining step is to animate the expansion of all the place elements so the sparks fly outward. In my sandbox, I added sliders to allow stepping through different parts of the effect. In the final animation, I broke it into two parts - the initial explosion and the dissipation of the embers.


// slider1 represents the explosion part of the firework
// when it rapidly expands to its final size.

$('#slider1').slider({
slide: function ( e, ui ) {
var pos = ui.value / 100,
pos2 = $('#slider2').slider('value') / 100;

$('.place').css({
top: -150 * pos + 30 * pos2,
left: -100 * pos,
width: 200 * pos,
height: 200 * pos + 90 * pos2
});
}
});


// slider2 represents the fading part of the firework
// as it starts to be affected by gravity and the sparks
// fizzle away.

$('#slider2').slider({
orientation: 'vertical',
slide: function ( e, ui ) {
var pos2 = ui.value / 100,
pos = $('#slider1').slider('value') / 100;

$('.place').css({
top: -150 * pos + 30 * pos2,
height: 200 * pos + 90 * pos2
});
}
});


Here you can see that by adjusting the position and size of each place DIV, forces the browser to render the 3D portion of the position which creates the look of an exploding sphere. By slightly offsetting the adjustments to top/height values, the firework will appear to be still traveling up while exploding and then slowly falls while the sparks fizzle away.

The animated version uses the GreenSock Animation Library to attach animations to these styles on each place element and adds them to a timeline so they can be sequenced and more easily controlled. It also has timings to fire several fireworks at a time - slightly staggered. The whole effect creates between 600 - 900 DIVs on each firing sequence. Combined this with the 3D styles and the browser is doing a lot of work.

I used the sandbox version as a reference to remove the animation from the equation. If you use Firefox, you can see it is a lot choppier to resize the firework with the sliders compared to Chrome. Once you start animating these elements, Firefox gets quite slow and drops many frames while Chrome is nice and smooth (although, that will depend on how the computer your using to view it). Its clear, at this point, that Chrome has the lead in both CSS3 3D features and the actual rendering performance. It will be interesting to see how this demo improves as more browsers implement the features and tweak their rendering engines to increase performance.