Appendix B: Building Data-Driven Web Applications
So far we’ve had a chance to see many of the tools and libraries for creating individual JavaScript visualizations, but we’ve only considered them in the context of a traditional web page. Today, of course, the web is much more than traditional web pages. Especially on desktop computers, web sites are effectively full featured software applications. (Even on mobile devices many “apps” are really just web sites enclosed in a thin wrapper.) When a web application is structured around data, there’s a good chance it can benefit from data visualizations. That’s exactly what we’ll consider in this appendix: how to integrate data visualization into a true web application.
The sections that follow will walk through the development of an example application driven by data. The source of the data will be Nike’s Nike+ service for runners. Nike sells many products and applications that let runners track their activities and save the results for analysis and review. In this appendix we’ll build a web application to retrieve that data from Nike and present it to a user. Nike, of course, has its own web app for viewing Nike+ data, and that app is far superior to the simple example of this appendix. We’re certainly not trying to compete with Nike; rather, we’re just using the Nike+ service to structure our example.
Note 1: The examples in this appendix are based on the version of the interface at the time of this writing. There have very likely been changes to the interface since then.
Unlike most other chapters, we won’t be considering multiple independent examples. Instead we’ll walk through the main stages in the development and testing of a single data-driven application. This includes:
- How to structure a web application using a framework or library.
- How to organize an application into models and views.
- How to incorporate visualizations in views.
- How to connect application models with an external REST API.
- How to support web browser conventions in a single page application.
Note 2: To use the Nike+ data in an actual product, you must register your application with Nike and get the necessary credentials and security keys. That process also grants access to the full documentation for the service, which is not publicly available. Since we’re not building a real application in this example, we won’t cover that step. We will, however, base the application on the Nike+ API, which is documented publicly on Nike’s developer web site. Because the example doesn’t include the credentials and security keys, it won’t be able to access the real Nike+ service. The book’s source code, however, does include actual Nike+ data that can be used to emulate the Nike+ service for testing and development.
Frameworks and Libraries
If we’re using JavaScript to add data visualizations to traditional web pages, we don’t have to worry too much about organizing and structuring our JavaScript. After all, it’s often a relatively small amount of code, especially compared to the HTML markup and CSS styles that are also part of the page. With web applications, however, the code can grow to be more extensive and more complex. To help keep our code organized and manageable, we’ll take advantage of a JavaScript application library, also called a framework.
Step 1: Select an Application Library
Deciding to use an application library might be easier than deciding which one to use. The number of these libraries has exploded in the past few years; there are now over 30 high quality libraries from which to choose. A good place to see all the alternatives is TodoMVC. That site serves as a showcase for application libraries; it shows how to implement a simple to-do application in each.
There is an important difference between these application libraries that can help narrow the choice: Is an application library a pure library or an application framework? Those terms are often used interchangeably, but there is an important distinction. A pure library functions like jQuery or other libraries we’ve used throughout this book. It provides a set of tools for our application, and we can use as many—or as little—of those tools as we like. An application framework, on the other hand, dictates exactly how the application should work. The code that we write must follow the strictures and conventions of the framework. Fundamentally the difference is about control. With a pure library our code is in control and the library is at our disposal. With a framework the framework code is in control, and we simply add the code that makes our application unique.
The main advantage of a pure library is flexibility. Our code is in control of the application, and we have full latitude to structure the application to our own requirements. That’s not always a good thing, however. The constraints of a framework can protect us from making poor design decisions. Some of the world’s best JavaScript developers are responsible for the popular frameworks, and they’ve put a lot of thought into what makes a good web application. There’s another benefit to frameworks: Because the framework assumes more responsibility for the application, there’s generally less code we’re required to write.
It’s worth noting this distinction between frameworks and pure libraries, but almost any web application can be built effectively with either. Both approaches provide the organization and structure necessary for a high quality application. For our example we’ll use the Backbone.js library. It is by far the most popular of the pure (non-framework) libraries, and it’s used by dozens of the largest sites on the web. The general approach that we’ll follow, however, (including tools such as Yeoman) work well with almost any popular application library.
Step 2: Install Development Tools
When you start building your first real web application, deciding how to begin can be a bit intimidating. One tool that can be a big help at this stage is Yeoman, which describes itself as “The Web’s Scaffolding Tool for Modern Webapps.” That’s a pretty accurate description. Yeoman can define and initialize a project structure for a large number of different web application frameworks, including Backbone.js. As we’ll see, it also sets up and configures most of the other tools we’ll need during the application’s development.
Before we can use Yeoman we must first install node.js. Node.js is a powerful application development platform all by itself, but we won’t need to worry about the details here. It is, however, the application platform required by many modern web development tools like Yeoman. To install node.js, follow the instructions on the node.js web site.
With node.js installed, we can install the main Yeoman application as well as everything necessary to create a Backbone.js application with one command. You can execute this command in the Terminal app (on Mac OS X) or from the Windows Command Prompt.
$ npm install -g generator-backbone
Step 3: Define a New Project
The development tools we just installed will make it easy to create a new web app project. First, with the commands below, we create a new folder (named running
) for our application and then cd
(change directory) into that folder.
$ mkdir running
$ cd running
From within that new folder, executing the command yo backbone
will initialize the project structure.
$ yo backbone
As part of the initialization, Yeoman will ask for permission to send diagnostic information (mainly which frameworks and features our app uses) back to the Yeoman developers. It will then give us a choice to add a few additional tools to the app. For our example we’ll skip any of the suggested options.
Out of the box I include HTML5 Boilerplate, jQuery, Backbone.js and Modernizr.
[?] What more would you like? (Press <space> to select)
❯⬡ Bootstrap for Sass
⬡ Use CoffeeScript
⬡ Use RequireJs
Yeoman will then do it’s magic, creating several sub-folders, installing extra tools and applications, and setting up reasonable defaults. As you watch all the pages and pages of installation information scroll by in your window, you can be glad that Yeoman is doing all this work for you. When Yeoman finishes, you’ll have a project structure like the one in figure ’s screenshot. It may not look exactly like the following since web applications may have changed since this text was written. Rest assured, though, that it will follow the best practices and conventions.
We’ll spend more time with most of these files and folders in the sections that follow, but here’s a quick overview of the project that Yeoman has set up for us.
app/
: The folder which will contain all the code for our app.bower.json
: A file that keeps track of all the third party libraries our app uses.Gruntfile.js
: A file that controls how to test and build our app.node_modules/
: A folder that contains the tools used to build and test our app.package.json
: A file that identifies the tools used to build and test our app.test/
: A folder that will contain the code we’ll write to test our app.
At this point Yeoman has set up a complete web app (albeit one that doesn’t do anything). You can execute the command grunt serve
from the command prompt to see it in a browser.
$ grunt serve
Running "serve" task
Running "clean:server" (clean) task
Running "createDefaultTemplate" task
Running "jst:compile" (jst) task
>> Destination not written because compiled files were empty.
Running "connect:livereload" (connect) task
Started connect web server on http://localhost:9000
Running "open:server" (open) task
Running "watch:livereload" (watch) task
Waiting...
The grunt
command runs one of the tools that’s part of the Yeoman package. When passed the serve
option, it cleans up the application folder, starts a web server to host the application, launches a web browser, and navigates to the skeleton app. You’ll see something like figure in your browser.
Congratulations! Our web app, as basic as it is, is now running.
Step 4: Add our Unique Dependencies
Yeoman sets up sensible defaults and tools for a new app, but our app needs a few JavaScript libraries that aren’t part of those defaults—Leaflet for maps and Flot for charts. The Moment.js library for dealing with dates and times will also come in handy, as will the Underscore.string library. We can add these libraries to our project with some simple commands. The --save
option tells bower to remember that our project depends on these libraries.
$ bower install leaflet --save
$ bower install flot --save
$ bower install momentjs --save
$ bower install underscore.string --save
Perhaps you’ve already begun to appreciate how tools like Yeoman make development easier. The simple commands above save us from having to find the libraries on the web, download the appropriate files, copy them to the right place in our project, and so on.
Even more importantly, Yeoman (technically, it’s the bower tool that’s part of the Yeoman package) automatically takes care of any additional libraries on which these libraries depend. The Flot library, for example, requires jQuery. When Yeoman installs Flot it will also check and make sure that jQuery is installed in the project. In our case it is because Backbone.js depends on it, but if jQuery weren’t already installed, Yeoman would automatically find it and install it as well.
For most libraries bower
can completely install all the necessary components and files. In the case of Leaflet, however, we need to perform a few extra steps. Change directory to the leaflet
folder within app/bower_components
. From there, run two commands to install the unique tools that Leaflet requires.
$ npm install
$ npm install jake -g
Executing the command jake
will then run all of Leaflet’s tests and, provided they pass, create a leaflet.js
library for our app.
$ jake
Checking for JS errors...
Check passed.
Checking for specs JS errors...
Check passed.
Running tests...
................................................................................
................................................................................
................................................................................
........................................
PhantomJS 1.9.7 (Mac OS X): Executed 280 of 280 SUCCESS (0.881 secs / 0.496 secs)
Tests ran successfully.
Concatenating and compressing 75 files...
Uncompressed: 217.22 KB (unchanged)
Compressed: 122.27 KB (unchanged)
Gzipped: 32.71 KB
All that’s left to do is adding the additional libraries into our HTML files. That’s easy enough. The main page for our app is index.html
in the app
folder. There’s already a block of code that includes jQuery, Underscore.js, and Backbone.js:
1 2 3 4 5 |
|
We can add our new libraries after Backbone.js.
1 2 3 4 5 6 7 8 9 10 11 |
|
Leaflet, as we saw in chapter 6, also requires it’s own style sheet. We add that to the top of index.html
just before main.css
.
1 2 3 4 |
|
Now that we’ve set up the structure of our app and installed the necessary libraries, it’s time to start development.
Models and Views
There are many application libraries available for web apps, and they each have their quirks, but most of the libraries agree on the key principles that should guide an app’s architecture. Perhaps the most fundamental of those principles is separating models from views. The code that keeps track of the core data for the app—the models—should be separate from the code that presents that data to the user—the views. Enforcing this separation makes it easier to update and modify either. If you want to present your data in a table instead of a chart, you can do that without any changes to the models. And if you need to change your data source from a local file to a REST API, you can do that without any changes to the views. We’ve been employing this principle in an informal way throughout the book. In all of the examples we’ve isolated the steps required to obtain and format our data from the steps we used to visualize it. Using an application library like Backbone.js gives us the tools to manage models and views more explicitly.
Step 1: Define the Application’s Models
Our running app is designed to work with Nike+, which provides details about runs—training runs, interval workouts, trail runs, races, and so on. The data set we want consists of nothing but runs, so our app’s core model is, naturally, a run.
The Yeoman tool makes it very easy to define a model for our app. A simple command defines a new model and creates the JavaScript files and scaffolding for that model.
$ yo backbone:model run
create app/scripts/models/run.js
invoke backbone-mocha:model
create test/models/run.spec.js
That command creates two new files: run.js
in the app/scripts/models/
folder and run.spec.js
in the test/
folder. Let’s take a look at the file Yeoman created for our model. It’s quite short.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
The first line is a comment that lists the global variables our model requires. In this case there are only two: Running
(that’s our app), and Backbone
. Next, in line 3, this file creates a .Models
property of the Running
object unless that property already exists.
When the browser encounters this line it will check to see if Running.Models
exists. If it does, then Running.Models
won’t be false
, and the browser never has to consider the second clause of the logical or (||
). The statement simply assigns Running.Models
to itself, so it has no practical effect. If Running.Models
does not exist, however, then Running.Models
evaluates to false
, and the browser will continue to the second clause, where it assigs an empty object ({}
) to Running.Models
. Ultimately, this statement makes sure that the object Running.Models
exists.
The rest of the code in the file is enclosed within an Immediately-Invoked Function Expression. If you haven’t seen this pattern before, it may look a little strange.
1 2 3 |
|
If we re-write the block as a single line, though, it might be easier to understand.
1 |
|
The statement defines a JavaScript function with a function expression, function () { /* ... */ }
, and then, with the concluding ()
, calls (technically invokes) that newly created function. All we’re really doing, therefore, is putting our code inside of a function and calling that function. You’ll see this pattern a lot in professional JavaScript because it protects a block of code from interfering with other code blocks in the application.
When you define a variable in JavaScript, that variable is a global variable, available everywhere in the code. As a consequence, if two different sections of code try to define the same global variable, those definitions will clash. This interaction can cause bugs that are very hard to find, as code in once section inadvertently interferes with code in a completely different section. To prevent this problem we can avoid using global variables, and the easiest way to do that in JavaScript is to define our variables inside a function. That’s the purpose of an immediately-invoked function expression. It makes sure that any variables our code defines are local to the function rather than global, and it prevents our code blocks from interfering with each other.
Step 2: Implement the Model
Our application really only needs this one model, and it’s already complete! That’s right, the scaffolding that Yeoman has set up for us is a complete and functioning model for a Run. In fact, if it weren’t for some quirks in Nike’s REST API, we wouldn’t have to touch the model code at all. We’ll address those quirks in a later section.
Before we move on to the next step, though, let’s spend a little time looking at what we can do with our newly created model. To do that we’ll make a temporary addition to the model code. We won’t use the following code in the final app; it’s only meant to show off what our model can already do.
From the Nike+ documentation we find that the URL to retrieve details about a run (Nike+ uses the more general term “activity”) is https://api.nike.com/v1/me/sport/activities/{activityId}
. The final part depends on the specific activity, so we’ll add only the general part of the URL to our model, in Line 2 below.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Now imagine that we want to get the details for a specific run from the Nike+ service. The run in question has a unique identifier of 2126456911
. If the Nike+ API followed typical conventions, we could create a variable representing that run, and get all its data, with the hypothetical two statements below. (We’ll consider the quirks of the actual Nike+ interface in a later section.)
1 2 |
|
Since many APIs do follow typical conventions, it’s worth spending a little bit of time understanding how that code works. The first statement creates a new instance of the Run model and specifies its identifier. The second statement tells Backbone to retrieve the model’s data from the server. Backbone would take care of all the communication with Nike+, including error handling, timeouts, parsing the response, and so on. Once the fetch completes, detailed information from that run will be available from the model. If we provide a callback function, we could output some of the details. For example:
1 2 3 4 5 6 7 |
|
The output in the browser’s console would be:
Run started at 2013-04-09T10:54:33Z
Duration: 0:22:39.000
Distance: 3.7524
Calories: 240
Not bad for a few simple lines of code. The code in this step, though, is really just a detour. Our application won’t use individual models in this way. Instead we’ll use an even more powerful Backbone.js feature: collections.
Step 3: Define the Application’s Collections
The model we created is designed to capture the data for a single run. Our users, however, aren’t interested in just a single run. They’d like to see all of their runs, dozens, hundreds, possibly thousands of them. We can handle all of these runs with a collection, or group of models. The collection is one of the core concepts of Backbone.js, and it will be a big help for our app. Let’s define a collection for all of the user’s runs.
Yeoman makes it easy to define and set up scaffolding for our collection. We execute the single command yo backbone:collection runs
from the command line. (Yes, we’re being very original and calling our collection of runs, well, runs.)
$ yo backbone:collection runs
create app/scripts/collections/runs.js
invoke backbone-mocha:collection
create test/collections/runs.spec.js
Yeoman does the same thing for collections as it did for models. It creates an implementation file (runs.js
in the app/scripts/collections/
folder) and a test file. For now, let’s take a look at runs.js
.
1 2 3 4 5 6 7 8 9 10 |
|
This file is even simpler than our model; the default collection only has a single property to indicate what type of model the collection contains. Unfortunately, Yeoman isn’t smart enough to handle plurals, so it assumes the name of the model is the same as the name of the collection. That’s not true for our app, as our model is a Run (singular) and the collection is Runs (plural). While we’re removing that s
, we can also add a property to specify the REST API for the collection. That’s a URL from the Nike+ service.
1 2 3 4 |
|
With those two small changes, we’re ready to take advantage of our new collection. (Aside from handling a few quirks with the Nike+ API; we’ll ignore that complication for now and address it later.) All we need to do is create a new instance of the Runs collection and then fetch its data.
1 2 |
|
That’s all it takes to build a collection containing the user’s runs. Backbone.js creates a model for each and retrieves the model’s data from the server. Even better, those run models are stored in a true Underscore.js collection, which gives us access to many powerful methods to manipulate and search through the collection. Suppose, for example, we want to find the total distance for all of a user’s runs. That’s a tailor-made for the Underscore.js reduce
function.
1 2 3 |
|
That code could tell us, for example, that the user has logged a total of 3358 km with Nike+.
As you may have noticed, we’re taking advantage of many utilities from Underscore.js in our Backbone.js application. That is not a coincidence. Jeremy Ashkenas is the lead developer for both projects.
Step 4: Define the Application’s Main View
Now that we have all the running data for a user, it’s time to present that data. We’ll do that with Backbone.js views. To keep our example simple, we’ll only consider two ways to show the running data. First we’ll display a table listing summary information about each run. Then, if the user clicks on a table row, we’ll show details about that specific run including any visualizations. The main view of our application will be the summary table, so let’s focus on that first.
A Backbone.js view is responsible for the presenting data to the user, and that data may be maintained in a collection or a model. For the main page of our app, we want to show summary information for all of a user’s runs. That view, therefore, is a view of the entire collection. We’ll call the view Summary.
The bulk of the table for this Summary view will be a series of table rows, where each row presents summary data about an individual run. That means we can simply create a view of a single Run model presented as a table row, and design our main Summary view to be made up (mostly) of many SummaryRow views. We can once again rely on Yeoman to set up the scaffolding for both of those types of views.
$ yo backbone:view summary
create app/scripts/templates/summary.ejs
create app/scripts/views/summary.js
invoke backbone-mocha:view
create test/views/summary.spec.js
$ yo backbone:view summaryRow
create app/scripts/templates/summaryRow.ejs
create app/scripts/views/summaryRow.js
invoke backbone-mocha:view
create test/views/summaryRow.spec.js
The scaffolding that Yeoman sets up is pretty much the same for each view; only the name varies. Here’s what a Summary view looks like.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
The overall structure of the file is the same as our model and our collection, but there’s a bit more going on in the view itself. Let’s step through view’s properties one at a time. The first property is template
. That’s where we define the exact HTML markup for the view, and we’ll look at this in more detail in the next step.
The tagName
property defines the HTML tag that our view will use as its parent. Yeoman defaults it to a generic <div>
, but we know that in our case it will be a <table>
. We’ll change that in a moment.
The id
and className
properties specify HTML id
attributes or class
values to add to the main container (in our case the <table>
). We could, for example, base some CSS styles on these values. For our example we’re not considering styles, so we can leave both properties blank or delete them entirely.
Next is the events
property. This object identifies user events (such as mouse clicks) that are relevant for the view. In the case of the Summary view, there are no events, so we can leave the object empty or simply delete it.
The last two properties, initialize
and render
, are both methods. Before we consider those, let’s see the Summary view after the tweaks mentioned above. Now that we’ve omitted the properties we won’t be using, we’re down to just the template
and tagName
properties, plus the initialize
and render
methods:
1 2 3 4 5 6 7 8 9 10 |
|
Now let’s look inside the last two methods, starting with initialize
. That method has a single statement (other than the return
statement that we just added). By calling listenTo
, it tells Backbone.js that the view wants to listen for events. The first parameter, which we’ll change to this.collection
, specifies the event target, so the statement says that the view wants to listen to events affecting the collection. The second parameter specifies the type of events. In this case the view wants to know whenever the collection changes. The final parameter is the function Backbone.js should call when the event occurs. Every time the Runs collection changes, we want Backbone.js to call the view’s render
method. That makes sense. Whenever the Runs collection changes, whatever we were displaying on the page before is now out-of-date. To make it current, our view should refresh its contents.
Most of the real work of a view takes place in its render
method. After all, this is the code that actually creates the HTML markup for the web page. Yeoman has gotten us started with a template (though again we need to change this.model
to this.collection
), but, in the case of a collection view, that’s not enough. The template takes care of the HTML for the collection as a whole, but it doesn’t handle the models that are part of the collection. For the individual runs, we can use the each
function from Underscore.js to iterate through the collection and render each run.
As you can see from the code below, we’ve also added a return this;
statement to each method. In a bit we’ll take advantage of this addition to chain together calls to multiple methods in a single, concise statement.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now we have to write the renderRun
method that handles each individual run. Here’s what we want that function to do:
- Create a new SummaryRow view for the run.
- Render that SummaryRow view.
- Append the resulting HTML to the
<tbody>
in the Summary view.
The code to implement those steps is straightforward, but it’s helpful to take each step one at a time.
- Create a new SummaryRow view:
new SummaryRow({model: run})
- Render that Summary Row view:
.render()
- Append the result:
this.$('tbody').append();
When we put the steps together we have the renderRun
method.
1 2 3 4 5 |
|
Most of the changes we made to the Summary view are also appropriate for the SummaryRow view, although we don’t need to add anything to the render
method. Here’s our first implementation of the SummaryRow. Note that we’ve set the tagName
property to 'tr'
because we want each run model presented as a table row.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now we have all the JavaScript code we need to show the main summary view for our app.
Step 5: Define the Main View Templates
So far we’ve developed the JavaScript code to manipulate our Summary and SummaryRow views. That code doesn’t generate the actual HTML markup, though. For that task we rely on templates. Templates are skeletal HTML markup with placeholders for individual values. Confining HTML markup to templates helps keep our JavaScript code clean, well-structured, and easy to maintain.
Just as there are many popular JavaScript application libraries, there are also many template languages. Our application doesn’t require any fancy template functionality, however, so we’ll stick with the default template process that Yeoman has set up for us. That process relies on a JST tool to process templates, and the tool uses the Underscore.js template language. It’s easy to see how this works through an example, so let’s dive in.
The first template we’ll tackle is the template for a SummaryRow. In our view we’ve already established that the SummaryRow is a <tr>
element, so the template only needs to supply the content that lives within that <tr>
. We’ll get that content from the associated Run model which, in turn, comes from the Nike+ service. Here’s an example activity that Nike+ could return.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
For a first implementation, let’s show the time of the run, as well as it’s duration, distance, and calories. Our table row, therefore, will have four cells with each cell holding one of these values. We can find the template in the app/scripts/templates
folder. It’s the summaryRow.ejs
file. By default, Yeoman sets it to a simple paragraph.
1 |
|
Let’s replace that with four table cells.
1 2 3 4 |
|
As placeholders for the cells’ content, we can use model attributes enclosed in special <%=
and %>
delimiters. The full SummaryRow template, therefore, is as follows.
1 2 3 4 |
|
The other template we need to supply is the Summary template. Since we’ve already set the view’s main tag to be a <table>
, this template should specify the content within that <table>
: a table header row plus an empty <tbody>
element (whose individual rows will come from the Run models).
1 2 3 4 5 6 7 8 9 |
|
Now we’re finally ready to construct the main view for our runs. The steps are quite straightforward.
- Create a new Runs collection.
- Fetch the data for that collection from the server.
- Create a new Summary view for the collection.
- Render the view.
Here’s the JavaScript code for those four steps.
1 2 3 4 |
|
We can access the constructed <table>
as the el
(short for element) property of the view. It will look something like the following.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
When we insert that markup in the page, our users can see a simple summary table listing their runs.
Time | Duration | Distance | Calories |
---|---|---|---|
2013-04-09T10:54:33Z | 0:22:39.000 | 3.7524 | 240 |
2013-04-07T12:34:40Z | 0:44:59.000 | 8.1724 | 569 |
2013-04-06T13:28:36Z | 1:28:59.000 | 16.068001 | 1200 |
2013-04-04T11:57:16Z | 0:58:44.000 | 9.623 | 736 |
2013-04-02T11:42:47Z | 0:22:37.000 | 3.6368 | 293 |
2013-03-31T12:44:00Z | 0:34:04.000 | 6.3987 | 445 |
2013-03-30T13:15:35Z | 1:29:31.000 | 16.0548 | 1203 |
2013-03-28T11:42:17Z | 1:04:09.000 | 11.1741 | 852 |
2013-03-26T12:21:52Z | 0:39:33.000 | 7.3032 | 514 |
2013-03-24T20:15:31Z | 0:33:49.000 | 6.2886 | 455 |
Step 6: Refine the Main View
Now we’re starting to get somewhere, though the table contents could use some tweaking. After all, does the last digit in a run of 16.068001 km really matter? Since Nike+ determines the attributes of our Run model, it might seem like we have no control over the values passed to our template. Fortunately, that’s not the case. If we look at the SummaryRow’s render
method, we can see how the template gets its values.
1 2 3 4 |
|
The template values come from a JavaScript object that we’re creating directly from the model. Backbone.js provided the toJSON
method that returns a JavaScript object corresponding to the model’s attributes. We can actually pass any JavaScript object to the template, even one we create ourselves within the render
method. Let’s rewrite that method to provide a more user-friendly summary view. We’ll take the model’s attributes one at a time.
First up is the date of the run. A date of “2013-04-09T10:54:33Z” isn’t very readable for average users, and it’s probably not even in their time zone. Working with dates and times is actually quite tricky, but the excellent Moment.js library can handle all of the complexity. Since we added that library to our app in an earlier section, we can take advantage of it now.
1 2 3 |
|
In the interest of brevity, we’re cheating a little with the code above because it converts the UTC timestamp to the local time zone of the browser. It would probably be more correct to convert it to the time zone for the run, which Nike+ provides in the data.
Next up is the run’s duration. It’s doubtful that we need to show the fractions of seconds that Nike+ includes, so let’s simply drop them from the attribute. (It would be more precise to round up or down, but assuming our users are not Olympic athletes in training, a second here or there won’t matter. Besides, Nike+ seems to always record these sub-second durations as “.000” anyway.)
1 |
|
The distance property can also use some adjustment. In addition to rounding it to a reasonable number of decimal places, we can convert from km to Miles for our US users. A single statement takes care of both.
1 2 3 |
|
The calories property is one value that’s fine as it is, so we’ll just copy it into our temporary object.
1 |
|
Finally, if you’re an avid runner you might have noticed that there’s an important value missing from the Nike+ attributes: the average pace for the run in minutes per mile. We have the data to calculate it, so let’s go ahead and add that as well.
1 2 3 4 |
|
Now we have a new object to pass to the template.
1 |
|
We’ll also need to modify both templates to match the new markup. Here’s the updated template for SummaryRows.
1 2 3 4 5 |
|
And here’s the Summary template with the additional column for pace.
1 2 3 4 5 6 7 8 9 10 |
|
Now we have a much improved summary view for our users.
Date | Duration | Distance | Calories | Pace |
---|---|---|---|---|
04/09/2013 | 0:22:39 | 2.33 Miles | 240 | 9:43 |
04/07/2013 | 0:44:59 | 5.08 Miles | 569 | 8:51 |
04/06/2013 | 1:28:59 | 9.98 Miles | 1200 | 8:54 |
04/04/2013 | 0:58:44 | 5.98 Miles | 736 | 9:49 |
04/02/2013 | 0:22:37 | 2.26 Miles | 293 | 10:00 |
03/31/2013 | 0:34:04 | 3.98 Miles | 445 | 8:33 |
03/30/2013 | 1:29:31 | 9.98 Miles | 1203 | 8:58 |
03/28/2013 | 1:04:09 | 6.94 Miles | 852 | 9:14 |
03/26/2013 | 0:39:33 | 4.54 Miles | 514 | 8:42 |
03/24/2013 | 0:33:49 | 3.91 Miles | 455 | 8:38 |
Views for Visualizations
Now that we’ve seen how to use Backbone.js views to separate data from its presentation, we can consider how to use the same approach for data visualizations. When the presentation is simple HTML markup—as in the previous section’s table—it’s easy to use templates to view a model. But templates aren’t sophisticated enough to handle data visualizations, so we’ll need to modify our approach for those.
The data from the Nike+ service offers lots of opportunity for visualizations. Each run, for example, may include a record of the user’s heart rate, instantaneous pace, and cumulative distance, recorded every 10 seconds. Runs may also include the user’s GPS coordinates captured every second. That type of data lends itself to both charts and maps, and in this section we’ll add both to our application.
Step 1: Define the Additional Views
As we did in the previous section, we’ll rely on Yeoman to create the scaffolding for our additional views. One view, which we’ll call Details, will act as the overall view for the details of an individual run. Within that view we’ll create three additional views each showing a different aspect of the run. We can think of these views in a hierarchy.
- Details: a detailed view of a single run
- Properties: the full set of properties associated with the run
- Chart: charts showing performance during the run
- Map: a map of the run’s route
To start the development of these views, we return to the command line and execute four Yeoman commands.
$ yo backbone:view details
$ yo backbone:view properties
$ yo backbone:view charts
$ yo backbone:view map
Step 2: Implement the Details View
The Details view is really nothing more than a container for its three children, so its implementation is about as easy as it gets. We create a new view for each of the children, render the view, and add the resulting markup to the Details. Here is the complete code for this view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Unlike the previous views we’ve created, this view doesn’t have an initialize
method. That’s because the Details view doesn’t have to listen for changes to the model, so there’s nothing to do during initialization. In other words, the Details view itself doesn’t actually depend on any of the properties of the Run model. (The child views, on the other hand, depend greatly on those properties.)
The render
method itself first clears out any existing content from its element. This line makes it safe to call the render
method multiple times. The next three statements create each of the child views. Notice that all of the child views have the same model, which is the model for the Details view as well. This capability is the power of the Model/View architecture; one data object—in our case a run—can be presented in many different ways. While the render
method creates each of these child views, it also calls their render
methods, and it appends the resulting content (their el
properties) into its own el
.
Step 3: Implement the Properties View
For the Properties view, we want to show all of the properties that Nike+ has associated with the run. Those properties are determined by the data returned by the Nike+ service; here’s an example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
That data can certainly benefit from a bit of cleanup to make it more user friendly. To do that we’ll take advantage of the Underscore.string library we added to the project before. We can make sure that library is available by “mixing it into” the main Underscore.js library. We’ll do that right at the start of the JavaScript file for the Properties view.
1 2 3 4 5 6 7 |
|
Notice that we’ve also added the global variable for Underscore.js (_
) to the initial comment in the file.
The most straightforward way to present this information in HTML is with a description list (<dl>
). Each property can be an individual item in the list, with a description term (<dt>
) holding the property name and the description data (<dd>
) its value. To implement this, we set the tagName
property of the view to be 'dl'
, and we create a generic list item template. Here’s the start of our Properties view code.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
And here’s the simple template that the view will use.
1 2 |
|
A quick glance at the Nike+ data shows that it contains nested objects. The metricSummary
property of the main object is itself an object. We need a function that will iterate through all the properties in the input object building the HTML markup as it does. A recursive function can be particularly effective here, since it can call itself whenever it reaches another nested object. Below, we add an obj2Html
method to our view. At it’s core, this method will use the Underscore.js reduce
function, which is well-suited to the task at hand.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
As we process each property, the first thing we can do is improve the key name. For example, we’d like to replace “startTime” with “Start Time”. That’s where Underscore.string comes in. Its humanize
function turns camelCase into separate words, and its titleize
function ensures each word begins with an uppercase letter. We’ll use chaining to perform both operations in one statement.
1 |
|
Now we can consider the value. If it is an array, we’ll replace it with a string that shows the array length.
1 2 3 |
|
Next we check to see if the value is an object. If it is, then we’ll call the obj2Html
method recursively.
1 2 |
|
For other types, we convert the value to a string, format it a bit with Underscore.string, and make use of our template.
1 2 3 4 |
|
There are a few other minor improvements we can make to the presentation which you can find in the book’s source code. The last piece of the View is implementing the render
method. In that method, we use toJSON
to get an object corresponding to the Run model, and then we start the obj2Html
recursion with that object.
1 2 3 4 |
|
The result is a complete picture of the properties of the run.
- Activity
- 2126456911
- Activity Type
- Run
- Start Time
- 2013-04-09t10:54:33z
- Activity Time Zone
- GMT-04:00
- Status
- Complete
- Device Type
- iPod
- Calories
- 240
- Fuel
- 790
- Distance
- 3.7524
- Steps
- 0
- Duration
- 0:22:39.000
- Weather
- Sunny
- Terrain
- Trail
- Shoes
- Neo Trail
- Emotion
- Great
- Speed Data
- [136 Items]
- Heartrate Data
- [136 Items]
- Distance Data
- [136 Items]
- Elevation Loss
- 114.400024
- Elevation Gain
- 109.00003
- Elevation Max
- 296.2
- Elevation Min
- 257
- Waypoints
- [266 Items]
Step 4: Implement the Map View
To show users maps of their runs we rely on the Leaflet library from chapter 6. Using the library will require some small modifications to the normal Backbone.js view implementation, but, as we’ll see, those same modifications will come in handy for other views as well. Leaflet builds its maps in a containing element in the page (typically a <div>
), and that containing element must have an id
attribute so that Leaflet can find it. Backbone.js will take care of adding that id
if we include an id
property in the view. That’s easy enough.
1 2 |
|
With <div id='map'></div>
available in the page’s markup, we can create a Leaflet map with the statement
1 |
|
We might be tempted to do that directly in the view’s render
method, but there’s a problem with that approach. Adding (and removing) elements in a web page requires a lot of computation by the browser. When JavaScript code does that frequently, the performance of the page can suffer significantly. To reduce this problem, Backbone.js tries to minimize the number of times it adds (or removes) elements, and one way to do that is to add many elements at once, rather than adding each element independently. It employs that approach when it implements a view’s render
method. Before adding any elements to the page, it lets the view finish constructing its entire markup. Only then does it add that markup to the page.
The problem here is that when render
is called the first time, there won’t (yet) be a <div id='map'></div>
anywhere in the page. If we call Leaflet, it won’t be able to find the container for its map, and it will generate an error. What we need to do is defer the part of render
that draws the map until after Backbone.js has added the map container to the page.
Fortunately, Underscore.js has a utility function called defer
to do just that. Instead of drawing the map directly in the render
method, we’ll create a separate method. Then, in the render
method, we’ll defer execution of that new method. Here’s what the code to do that looks like.
1 2 3 4 5 6 7 |
|
As you can see, we’re actually using a couple of Underscore.js functions in our render
method. In addition to defer
, we also take advantage of bind
. The latter function ensures that the this
value when drawMap
is eventually called is the same as the this
value within the view.
There’s one change we can make to further improve this implementation. Although there won’t be a <div id='map'></div>
in the page when render
is first called, that element will exist in subsequent calls to render
. In those cases we don’t need to defer the execution of drawMap
. That leads to the following code for our render
method.
1 2 3 4 5 6 7 8 |
|
As long as we’re making optimizations, let’s also change the initialize
method slightly. The default method that Yeoman creates is
1 2 3 |
|
For the Map view, however, we don’t really care if any property of the Run model changes. The only property the view needs is the gps
property. We can, therefore, tell Backbone.js to only bother us if that specific property changes.
1 2 3 4 |
|
If you’re wondering “Why would the
gps
property of the Run model ever change?” be patient. We’ll get to that when we cover the quirks of the Nike+ REST API.
With the preliminaries out of the way, we can implement the drawMap
function. It turns out to be a very easy implementation. The steps are
- Make sure the model has a
gps
property and there are waypoints associated with it. - If an old map exists, remove it.
- Extract the GPS coordinates from the waypoints array.
- Create a path using those coordinates.
- Create a map that contains that path and draw the path on the map.
- Add the map tiles.
The resulting code is a straightforward implementation of those steps.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
As you can see from the code, we’re storing a reference to the Leaflet map object as a property of the view. From within the view, we can access that object using this.map
.
The result is a nice map of the run’s route in figure .
Step 5: Implement the Charts View
The last remaining view that we need to implement is the Charts view where we want to show pace, heart rate, and elevation during the run. This view is the most complex, but nearly all of the code is identical to the Tracking Values example in chapter 2, so there’s no need to repeat that material here. The source code for the book includes the complete implementation. If you’re looking in detail at that implementation, here a few points to note.
- Just as Leaflet and the map container, Flot expects a container for its chart to be present in the web page. We can use the same
defer
trick to prevent Flot errors. - Nike+ returns at least four types of charts as metrics: distance, heart rate, speed, and GPS signal strength. We really only care about the first two. At first, it might seem easiest to calculate pace from speed, but speed isn’t present in all activities. Distance, however, is present, and we can derive pace from distance and time.
- If GPS waypoint data is available, we can also graph elevation, but that data is in a separate attribute of the model (not the metrics attribute).
- At least as of this writing, there’s a bit of a bug in Nike’s response for GPS data. It claims that the measurements are on the same time scale as the other metrics (every 10 seconds), but, in fact, the GPS measurements are reported on different intervals. To work around this bug, we ignore the reported interval, and calculate one ourselves. Also, we want to normalize the elevation graph to the same time scale as all the others. Doing that will give us the additional benefit of averaging the GPS elevation data; averaging is useful here because GPS elevation measurements aren’t generally very accurate.
You can see the interactive result in figure .
Connecting with the Nike+ Service
Although our example application relies on the Nike+ service for its data, we haven’t looked at the details of that service’s interface. As I’ve mentioned, Nike+ doesn’t quite conform to common REST API conventions that application libraries such as Backbone.js expect. But Nike+ isn’t very unusual in that regard. There really isn’t a true standard for REST APIs, and many other services take approaches similar to Nike+. Fortunately Backbone.js anticipates this variation. As we’ll see in the following steps, extending Backbone.js to support REST API variations isn’t all that difficult.
Step 1: User Authorization
As you might expect, Nike+ doesn’t allow anyone on the internet to retrieve details for any user’s runs. Users expect at least some level of privacy for that information. Before our app can retrieve any running information, therefore, it will need the user’s permission. We won’t go into the details of that process here, but its result will be an authorization_token
. This object is an arbitrary string that our app will have to include with every Nike+ request. If the token is missing or invalid, Nike+ will deny our app access to the data.
Up until now we’ve let Backbone.js handle all of the details of the REST API. Next, we’ll have to modify how Backbone.js constructs its AJAX calls. Fortunately, this isn’t as tricky as it sounds. All we need to do is add a sync
method to our Runs collection. When a sync
method is present in a collection, Backbone.js calls it whenever it makes an AJAX request. (If there is no such method for a collection, Backbone.js calls its primary Backbone.sync
method.) We’ll define the new method directly in the collection.
1 2 3 4 5 |
|
As you can see, sync
is passed a method
(GET
, POST
, etc.), the collection in question, and an object containing options for the request. To send the authorization token to Nike+, we can add it as a parameter using this options
object.
1 2 3 4 5 6 7 |
|
The first line in the method makes sure that the options
parameter exists. If the caller doesn’t provide a value, we set it to an empty object ({}
). The next statement adds a data
property to the options
object using the extend
utility from Underscore.js. The data
property is itself an object, and in it we store the authorization token. We’ll look at how to do that next, but first let’s finish up the sync
method. Once we’ve added the token, our request is a standard AJAX request so we can let Backbone.js take it from there by calling Backbone.sync
.
Now we can turn our attention to the settings
object from which our sync
method obtained the authorization token. We’re using that object to hold properties related to the collection as a whole. It’s the collection’s equivalent of a model’s attributes. Backbone.js doesn’t create this object for us automatically, but it’s easy enough to do it ourselves. We’ll do it in the collection’s initialize
method. That method accepts two parameters, an array of models for the collection and any collection options.
1 2 3 4 5 6 7 8 |
|
The first statement in the initialize
method defines a settings
object for the collection, and it establishes default values for that object. Since there isn’t an appropriate default value for the authorization token, we’ll use an empty string.
The next statement makes sure that the options
object exists. If none is passed as a parameter, we’ll at least have an empty object.
The final statement extracts all the keys in the settings, finds any values in the options
object with the same keys, and updates the settings
object by extending it with those new key values. Once again, we take advantage of some Underscore.js utilities: extend
and pick
.
When we first create the Runs collection, we can pass the authorization token as a parameter. We supply an empty array as the first parameter because we don’t have any models for the collection. Those will come from Nike+. In the code fragment below we’re using a dummy value for the authorization token. A real application would use code that Nike provides to get the true value.
1 2 3 |
|
With just a small bit of extra code we’ve added the authorization token to our AJAX requests to Nike+.
Step 2: Accepting the Nike+ Response
When our collection queries Nike+ for a list of user activities, Backbone.js is prepared for a response in a particular format. More specifically, Backbone.js expects the response to be a simple array of models.
1 2 3 4 5 6 |
|
In fact, however, Nike+ returns its response as an object. The array of activities is one property of the object.
1 2 3 4 5 6 7 8 9 |
|
To help Backbone.js cope with this response we add a parse
method to our collection. The job of that function is to take the response that the server provides and return the response that Backbone.js expects. In our case that’s very easy to do. We just return the data
property of the response.
1 2 3 4 5 |
|
Step 3: Paging the Collection
The next aspect of the Nike+ API we’ll tackle is its paging. When we request the activities for a user, the service doesn’t normally return all of them. Users may have thousands of activities stored in Nike+, and returning all of them at once might overwhelm the app. It can certainly add a noticeable delay as the app would have to wait for the entire response before it could process it. To avoid this problem, Nike+ divides user activities into pages, and it responds with one page of activities at a time. We’ll have to adjust our app for that behavior, but we’ll gain the benefit of a more responsive user experience when we do.
The first adjustment we’ll make is in our request. We can add parameters to that request to indicate how many activities we’re prepared to accept in the response. The two parameters are offset
and count
. The offset
tells Nike+ which activity we want to be first in the response, while count
indicates how many activities Nike+ should return. If we wanted the first 20 activities, for example, we can set offset
to 1
and count
to 20
. Then, to get the next 20 activities, we’d set offset
to 21
(and keep count
at 20
).
We add these parameters to our request the same way we added the authorization token, in the sync
method.
1 2 3 4 5 6 7 8 9 10 11 |
|
We will also have to provide default values for those settings during initialization.
1 2 3 4 5 6 |
|
Those values will get the first 25 activities, but that’s only a start. Our users will probably want to see all of their runs, not just the first 25. To get the additional activities we’ll have to make more requests to the server. Once we get the first 25 activities we can request the next 25. And once those arrive we can ask for 25 more. We’ll keep at this until we either reach some reasonable limit or the server runs out of activities.
First we define a reasonable limit as another settings value. In the code below we’re using 10000
as that limit.
1 2 3 4 5 6 7 |
|
Next we need to modify the fetch
method for our collection since the standard Backbone.js fetch
can’t handle paging. We’re going to do three things in our implementation of the method.
- Save a copy of whatever options Backbone.js is using for the request.
- Extend those options by adding a callback function when the request succeeds.
- Pass control to the normal Backbone.js
fetch
method for collections.
Each of those steps is a line in the implementation below. The last one might seem a little tricky, but it makes sense if you take it one step at a time. The expression Backbone.Collection.prototype.fetch
refers to the normal fetch
method of a Backbone.js collection. We execute this method using .call()
so that we can set the context for the method to be our collection. That’s the first this
parameter of call
. The second parameter holds the options for fetch
, which are just the extended options we created in step 2.
1 2 3 4 5 6 7 8 9 |
|
By adding a success
callback to the AJAX request, we’re asking to be notified when the request completes. In fact, we’ve said that we want the this.fetchMore
function to be called. It’s time to write that function; it too is a method of the Runs collection. This function checks to see if there are more activities left. If so, it executes another call to Backbone.js’s regular collection fetch
just as above.
1 2 3 4 5 |
|
Since fetchMore
is looking at the settings to decide when to stop, we’ll need to update those values. Because we already have a parse
method, and because Backbone calls this method with each response, that’s a convenient place for the update. Let’s add a bit of code before the return statement. If the number of activities that the server returns is less than the number we asked for, then we’ve exhausted the list of activities. We’ll set the offset
to the max
so fetchMore
knows to stop. Otherwise, we increment offset
by the number of activities.
1 2 3 4 5 6 7 8 |
|
The code we’ve written so far is almost complete, but it has a problem. When Backbone.js fetches a collection, it assumes that it’s fetching the whole collection. By default, therefore, each fetched response replaces the models already in the collection with those in the response. That behavior is fine the first time we call fetch
, but it’s definitely not okay for fetchmore
, which is meant to add to the collection instead of replacing it. Fortunately, it’s easy to tweak this behavior by setting the remove
option.
In our fetch
method we set that option to true
so Backbone.js will start a new collection.
1 2 3 4 5 6 7 8 9 10 |
|
Now, in the fetchMore
method we can reset this option to false
, and Backbone.js will add to models instead of replacing them in the collection.
1 2 3 4 5 6 |
|
There is still a small problem with the fetchMore
method. That code references properties of the collection (this.fetchoptions
and this.settings
) but the method will be called asynchronously when the AJAX request completes. When that occurs, the collection won’t be in context, so this
won’t be set to the collection. To fix that, we can bind fetchMore
to the collection during initialization. Once again an Underscore.js utility function comes in handy.
1 2 |
|
For the final part of this step we can make our collection a little more friendly to code that uses it. In order to keep fetching additional pages, we’ve set the success
callback for the fetch
options. What happens if the code that uses our collection had its own callback? Unfortunately, we’ve erased that callback to substitute our own. It would be better to simply set aside an existing callback function and then restore it once we’ve finished fetching the entire collection. We’ll do that first in our fetch
method. Here’s the full code for the method.
1 2 3 4 5 6 7 8 9 10 11 |
|
Now we can execute that callback in fetchMore
when we’ve exhausted the server’s list.
1 2 3 4 5 6 7 8 |
|
Step 4: Dynamically Updating the View
By fetching the collection of runs in pages we’ve made our application much more responsive. We can start displaying summary data for the first 25 runs even while we’re waiting to retrieve the rest of the user’s runs from the server. To do that effectively, though, we need to make a small change to our Summary view. As it stands now, our view is listening for any changes to the collection. When a change occurs, it renders the view from scratch.
1 2 3 4 |
|
Every time we fetch a new page of runs, the collection will change and our code will re-render the entire view. That’s almost certainly going to be annoying to our users, as each fetched page will cause the browser to temporarily blank the page and then refill it. What we would really like to do instead is only render views for the newly added models, leaving existing model views alone. To do that, we can listen for an 'add'
event instead of a 'change'
event. And when this event triggers, we can just render the view for that model. We’ve already implemented the code to create and render a view for a single Run model; it’s the renderRun
method. Our Summary view, therefore, can be modified as below.
1 2 3 4 |
|
Now as our collection fetches new Run models from the server, they’ll be added to the collection, triggering an 'add'
event, which our view captures. The view then renders each Run on the page.
Step 5: Filtering the Collection
Although our app is only interested in running, the Nike+ service supports a variety of athletic activities. When our collection fetches from the service, the response will include those other activities as well. To avoid including them in our app, we can filter them from the response.
We could filter the response manually, checking every activity and removing those that weren’t runs. That’s a bit of work, however, and Backbone.js gives us an easier approach. To take advantage of Backbone.js, we’ll first add a validate
method to our Run model. This method takes as parameters the attributes of a potential model as well as any options used when creating or modifying it. In our case we only care about the attributes. We’ll check to make sure the activityType
equals "RUN"
.
1 2 3 4 5 6 |
|
You can see from the code above how validate
functions should behave. If there is an error in the model, then validate
returns a value. The specifics of the value don’t matter as long as JavaScript considers it true. If there is no error, then validate
doesn’t need to return anything at all.
Now that our model has a validate
method, we need to make sure Backbone.js calls it. Backbone.js automatically checks with validate
whenever a model is created or modified by the code, but it doesn’t normally validate responses from the server. In our case, however, we do want to validate the server responses. That requires that we set the validate
property in the fetch
options for our Runs collection. Here’s the full fetch
method with this change included.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now when Backbone.js receives server responses, it passes all of the models in those responses through the model’s validate
method. Any model that fails validation is removed from the collection, and our app never has to bother with activities that aren’t runs.
Step 6: Parsing the Response
As long as we’re adding code to the Run model, there’s another change that will make Backbone.js happy. Backbone.js requires models to have an attribute that makes each object unique. It can use this attribute to distinguish one Run from any other. By default Backbone.js expects models to have an id
attribute for this purpose, as that’s a common convention. Nike+, however, doesn’t have an id
attribute for its runs. Instead, the service uses the activityId
attribute. We can tell Backbone.js about this with an extra property in the model.
1 2 |
|
This property lets Backbone.js know that for our Runs, the activityId
property is the unique identifier.
Step 7: Retrieving Details
So far we’ve been relying on the collection’s fetch method to get running data. That method retrieves a list of runs from the server. When Nike+ returns a list of activities, however, it doesn’t include the full details of each activity. It returns summary information, but it omits the detailed metrics arrays and any GPS data. Getting those details requires additional requests, so we need to make one more change to our Backbone.js app.
We’ll first request the detailed metrics which are the basis for the Charts view. When the Runs collection fetches its list of runs from the server, each Run model will initially have an empty metrics
array. To get the details for this array, we must make another request to the server with the activity identifier included in the request URL. For example, if the URL to get a list of runs is https://api.nike.com/v1/me/sport/activities
, then the URL to get the details for a specific run, including its metrics, is https://api.nike.com/v1/me/sport/activities/2126456911
. The number 2126456911
at the end of that URL is the run’s activityId
.
Because of the steps we’ve taken earlier in this section, Backbone.js makes it easy to get these details. All we have to do is fetch
the model.
1 |
|
Backbone.js knows the root of the URL because we set that in the Runs collection (and our model is a member of that collection). Backbone.js also knows that the unique identifier for each run is the activityId
because we set that property in the previous step. And, fortunately for us, Backbone.js is smart enough to combine those bits of information and make the request.
We will have to help Backbone.js in one respect, though. The Nike+ requires an authorization token for all requests, and so far we’ve only added code for that token to the collection. We have to add the same code to the model. The code below is almost identical to the code from step 1 in this section. We first make sure that the options
object exists, then we extend it by adding the authorization token. Finally, we defer to the regular Backbone.js sync
method. In Line 7 below, we’re getting the value for the token directly from the collection. We can use this.collection
here because Backbone.js sets the collection
property of the model to reference the collection it belongs to.
1 2 3 4 5 6 7 8 9 10 11 |
|
Now we have to decide when and where to call a model’s fetch
method. We don’t actually need the metrics details for the summary view on the main page of our app; we should only bother getting the data when we’re creating a Details view. We can conveniently do that in the view’s initialize
method.
1 2 3 4 5 6 7 |
|
You might think that the asynchronous nature of the request could cause problems for our view. After all, we’re trying to draw the charts when we render the newly created view. Won’t it draw the charts before the server has responded (that is, before we have any data for the charts)? In fact, it’s almost guaranteed that our view will be trying to draw its charts before the data is available. Nonetheless, because of the way we’ve structured our views, there is no problem.
The magic is in a single statement in the initialize
method of our Charts view.
1 2 3 4 5 |
|
That statement tells Backbone.js that our view wants to know whenever the metrics
(or gps
) property of the associated model changes. When the server responds to a fetch
and updates that property, Backbone.js calls the view’s render
method and it will try (again) to draw the charts.
There’s quite a lot going on in this process, so it may help to look at it one step at a time.
- The application calls the
fetch
method of a Runs collection. - Backbone.js sends a request to the server for a list of activities.
- The server’s response includes summary information for each activity, which Backbone.js uses to create the initial Run models.
- The application creates a Details view for a specific Run model.
- The
initialize
method of this view calls thefetch
method of the particular model. - Backbone.js sends a request to the server for that activity’s details.
- Meanwhile, the application renders the Details view it just created.
- The Details view creates a Charts view and renders that view.
- Because there is no data for any charts, the Charts view doesn’t actually add anything to the page, but it is waiting to hear of any relevant changes to the model.
- Eventually the server responds to the request in step 6 with details for the activity.
- Backbone.js updates the model with the new details and notices that, as a result, the
metrics
property has changed. - Backbone.js triggers the change event for which the Charts view has been listening.
- The Charts view receives the event trigger and again renders itself.
- Because chart data is now available, the
render
method is able to create the charts and add them to the page.
Whew! It’s a good thing that Backbone.js takes care of all that complexity.
At this point we’ve managed to retrieve the detailed metrics for a run, but we haven’t yet added any GPS data. Nike+ requires an additional request for that data, so we’ll use a similar process. In this case, though, we can’t rely on Backbone.js because the URL for the GPS request is unique to Nike+. That URL is formed by taking the individual activity’s URL and appending /gps
. A complete example would be https://api.nike.com/v1/me/sport/activities/2126456911/gps
.
To make the additional request we can add some code to the regular fetch
method. We’ll request the GPS data at the same time Backbone.js asks for the metrics details. The basic approach, which the code fragment below illustrates, is simple. We’ll first see if the activity even has any GPS data. We can do that by checking the isGpsActivity
property which the server provides on activity summaries. If it does, then we can request it. In either case we also want to execute the normal fetch
process for the model. We do that by getting a reference to the standard fetch
method for the model (Backbone.Model.prototype.fetch
) and then calling that method. We pass it the same options
passed to us.
1 2 3 4 5 6 7 |
|
Next, to make the request to Nike+ we can use jQuery’s AJAX function. Since we’re asking for Javascipt objects (JSON data), the $.getJSON
function is the most appropriate. First we set aside a reference to the run by assigning this
to the local variable model
. We’ll need that variable because this
won’t reference the model when jQuery executes our callback. Then we call $.getJSON
with three parameters. First is the URL for the request. We get that from Backbone.js by calling the url
method for the model and appending the trailing /gps
. The second parameter are data values to be included with the request. As always we need to include an authorization token. Just as we did above, we can get that token’s value from the collection. The final parameter is a callback function that JQuery executes when it receives the server’s response. In our case the function simply sets the gps
property of the model to the response data.
1 2 3 4 5 6 7 8 9 |
|
Not surprisingly, the process of retrieving GPS data works the same way as retrieving the detailed metrics. Initially our Map view won’t have the data it needs to create a map for the run. Because it’s listening to changes to the gps
property of the model, however, it will be notified when that data is available. At that point it can complete the render
function and the user will be able to view a nice map of the run.
Putting it All Together
At this point in the appendix we have all the pieces for a simple data-driven web application. Now we’ll take those pieces and assemble them into the app. At the end of this section we’ll have a complete application. Users start the app by visiting a web page, and our JavaScript code takes it from there. The result is a single page application, or SPA. SPAs have become popular because JavaScript code can respond to user interaction immediately in the browser, which is much quicker than traditional web sites communicating with a server located halfway across the internet. Users are often pleased with the snappy and responsive result.
Even though our app is executing in a single web page, our users still expect certain behaviors from their web browsers. They expect to be able to bookmark a page, share it with friends, or navigate using the browser’s forward and back buttons. Traditional web sites can rely on the browser to support all of those behaviors, but a single page application can’t. As we’ll see in the steps that follow, we have to write some additional code to give our users the behavior they expect.
Step 1: Create a Backbone.js Router
So far we’ve looked at three Backbone.js components, Models, Collections, and Views, all of which may be helpful in any JavaScript application. The fourth component, the Router, is especially helpful for single page applications. You won’t be surprised to learn that we can use Yeoman to create the scaffolding for a Router.
$ yo backbone:router app
create app/scripts/routes/app.js
invoke backbone-mocha:router
create test/routers/app.spec.js
Notice that we’ve named our router app
. As you might expect from this name, we’re using this router as the main controller for our application. That approach has pros and cons. Some developers feel that a router should be limited strictly to routing, while others view the router as the natural place to coordinate the overall application. For a simple example such as our’s, there isn’t really any harm in adding a bit of extra code to the router to control the app. In complex applications, however, it might be better to separate routing from application control. One of the nice things about Backbone.js is that it’s happy to support either approach.
With the scaffolding in place, we can start adding our router code to the app.js
file. The first property we’ll define are the routes
. This property is an object whose keys are URL fragments and whose values are methods of the router. Here’s our starting point.
1 2 3 4 5 6 |
|
The first route has an empty URL fragment (''
). When a user visits our page without specifying a path, the Router will call its summary
method. If, for example, we were hosting our app using the greatrunningapp.com
domain name, then users entering http://greatrunningapp.com
in their browsers would trigger that route. Before we look at the second route, let’s see what the summary
method does.
The code is exactly what we’ve seen before. The summary
method creates a new Runs Collection, fetches that collection, creates a Summary View of the collection, and renders that view onto the page. Users visiting the home page for our app will see a summary of their runs on the page.
1 2 3 4 5 6 7 |
|
Now we can consider our second route. It has a URL fragment of runs/:id
. The runs/
part is a standard URL path while :id
is how Backbone.js identifies an arbitrary variable. With this route we’re telling Backbone.js to look for a URL that starts out as http://greatrunningapp.com/runs/
and to consider whatever follows as the value for the id
parameter. We’ll use that parameter in the router’s details
method. Here’s how we’ll start the development of that method.
1 2 3 4 5 6 7 |
|
As you can see, the code is almost the same as the summary method, except we’re only showing a single run instead of the whole collection. We create a new Run model, set its id
to the value in the URL, fetch the model from the server, create a Details view, and render that view on the page.
The router lets users go straight to an individual run by using the appropriate URL. A URL of http://greatrunningapp.com/runs/2126456911
, for example, will fetch and display the details for the run that has an activityId
equal 2126456911
. Notice that the router doesn’t have to worry about what specific attribute defines the model’s unique identifier. It uses the generic id
property. Only the model itself needs to know the actual property name that the server uses.
With the router in place, our single page application can support multiple URLs. One shows a summary of all runs while others show the details of a specific run. Because the URLs are distinct, our users can treat them just like different web pages. They can bookmark them; they can email them, or they can share them on social networks. And whenever they or their friends return to a URL, it will show the same contents as before. That’s exactly how users expect the web to behave.
There is another behavior that users expect, though, that we haven’t yet supported. Users expect to use their browser’s back and forward buttons to navigate through their browsing histories. Fortunately, Backbone.js has a utility that takes care of that functionality. It’s the history feature, and we can enable it during the app Router’s initialization.
1 2 3 4 |
|
For our simple app, that’s all we have to do to handle browsing histories. Backbone.js takes care of everything else.
Support for multiple URLs will probably require some configuration of your web server. More specifically, you’ll want the server to map all URLs to the same
index.html
file. The details of this configuration depend on the web server technology. With open source apache servers, the.htaccess
file can define the mapping.
Step 2: Support Run Models Outside of Any Collection
Unfortunately, if we try to use the code above with our existing Run model, we’ll run into some problems. First among them is the fact that our Run model relies on its parent collection. It finds the authorization token, for example, using this.collection.settings.authorization_token
. When the browser goes directly to the URL for a specific run, however, there won’t be a collection. We can fix that by providing the token to the Run model when we create it at line 20. We can also make its value an option passed to the collection on creation in line 12. Here’s the code that results.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Now we need to modify the Run model to use this new parameter. We’ll handle the token the same way we do in the Runs collection. We start by defining default values for all the settings. Unlike the collection, the only setting our model needs is the authorization_token
. Next we make sure that we have an options
object. If none was provided, we create an empty one. For the third step we check to see if the model is part of a collection. We can do that by looking at this.collection
. If that property exists, then we grab any settings from the collection and override our defaults. The final step overrides the result with any settings passed to our constructor as options. When, as in the code above, our router provides an authorization_token
value, that’s the value our model will use. When the model is part of a collection, there is no specific token associated with the model. In that case we fall back to the collection’s token.
1 2 3 4 5 6 7 8 9 10 11 |
|
Now that we have an authorization token, we can add it to the model’s AJAX requests. The code is again pretty much the same as our code in the Runs collection. We’ll need a property that specifies the URL for the REST service, and we’ll need to override the regular sync
method to add the token to any requests.
1 2 3 4 5 6 7 8 9 |
|
This extra code takes care of the authorization, but there’s still a problem with our model. In the previous section Run models only existed as part of a Runs collection, and the act of fetching that collection populated each of its models with summary attributes including, for example, isGpsActivity
. The model could safely check that property whenever we tried to fetch the model details and, if appropriate, simultaneously initiate a request for the GPS data. Now, however, we’re creating a Run model on its own without the benefit of a collection. When we fetch the model, the only property we’ll know is the unique identifier. We can’t decide whether or not to request GPS data, therefore until after the server responds to the fetch.
To separate the request for GPS data from the general fetch we can move that request to its own method. The code is the same as before (except, of course, we get the authorization token from local settings).
1 2 3 4 5 6 7 8 9 10 |
|
To trigger this method, we’ll tell Backbone.js that whenever the model changes, it should call the fetchGps
method. Backbone.js will detect just such a change when the fetch
response arrives and populates the model, at which time our code can safely check isGpsActivity
and make the additional request.
1 2 |
|
Step 3: Let Users Change Views
Now that our app can correctly display two different views, it’s time to let our users in on the fun. For this step, we’ll give them an easy way to change back and forth between the views. Let’s first consider the summary view. It would be nice if a user could click on any run that appears in the table and be instantly taken to the detailed view for that run.
Our first decision is where to put the code that listens for clicks. At first thought, it might seem like the SummaryRow view is a natural place for that code. That view is responsible for rendering the row, so it seems logical to let that view handle events related to the row. If we wanted to do that, Backbone.js makes it very simple. All we need is an extra property and an extra method in the view. They could look like the following.
1 2 3 4 5 6 7 |
|
The events
property is an object that lists the events of interest to our view. In this case there’s only one, and it is the click
event. The value, in this case clicked
identifies the method that Backbone.js should call when the event occurs. We’ve skipped the details of that method for now.
There is nothing technically wrong with this approach, and if we were to continue the implementation it would probably work just fine. It is, however, very inefficient. Consider a user that has hundreds of runs stored on Nike+. Their summary table would have hundreds of rows, and each row would have its own function listening for click
events. Those event handlers can use up a lot of memory and other resources in the browser and make our app sluggish. Fortunately there’s a different approach that’s far less stressful to the browser.
Instead of having potentially hundreds of event handlers each listening for clicks on a single row, we’d be better off with one event handler listening for clicks on all of the table rows. Since the Summary view is responsible for all of those rows, it’s the natural place to add that handler. We can still take advantage of Backbone.js to make its implementation easy by adding an events
object to our view. In this case we can do even better, though. We don’t care about click
events on the table header; only the rows in the table body matter. By adding a jQuery-style selector after the event name we can restrict our handler to elements that match that selector.
1 2 3 4 |
|
The code above asks Backbone.js to watch for click
events within the <tbody>
element of our view. When an event occurs, Backbone.js will call the clicked
method of our view.
Before we develop any code for that clicked
method, we’re going to need a way for it to figure out which specific run model the user has selected. The event handler will be able to tell which row the user clicked, but how will it know which model that row represents? To make the answer easy for the handler, we can embed the necessary information directly in the markup for the row. That requires a few small adjustments to the renderRun
method we created earlier.
The revised method still creates a SummaryRow view for each model, renders that view, and appends the result to the table body. Now, though, we’ll add one extra step just before the row is added to the page. We add a special attribute, data-id
, to the row and set its value equal to the model’s unique identifier. We use data-id
because the HTML5 standard allows any attribute with a name that begins data-
. Custom attributes in this form won’t violate the standard and won’t cause browser errors.
1 2 3 4 5 6 |
|
The resulting markup for a run with an identifier of 2126456911
would look like the following example.
1 2 3 4 5 6 7 |
|
Once we’ve made sure that the markup in the page has a reference back to the run models, we can take advantage of that markup in our clicked
event handler. When Backbone.js calls the handler, it passes it an event object. From that object we can find the target of the event. In the case of a click
event, the target is the HTML element on which the user clicked.
1 2 |
|
From the markup shown above, it’s clear that most of the table row is made up of table cells (<td>
elements), and so a table cell will be the likely target of the click event. We can use the jQuery parents
function to find the table row that is the parent of the click target. Once we’ve found that row, it’s an easy matter to extract the data-id
attribute value. To be on the safe side we’ll also handle the case in which the user somehow manages to click on the table row itself rather than an individual table cell.
1 2 3 4 |
|
After retrieving the attribute value our view knows which run the user selected; now it has to do something with the information. Though it might be tempting to have the Summary view directly render the Details view for the run, that action would not be appropriate. A Backbone.js view should only take responsibility for itself and any child views that it contains. That approach allows the view to be safely re-used in a variety of contexts. Our Summary view, for example, might well be used in a context in which the Details view wasn’t even available. In that case trying to switch directly to the Details view would, at best, generate an error.
Because the Summary view cannot itself respond to the user clicking on a table row, it should instead follow the hierarchy of the application and, in effect, pass the information “up the chain of command.” Backbone.js provides a convenient mechanism for this type of communication: custom events. Instead of responding directly to the user click, the Summary view triggers a custom event. Other parts can listen for this event and respond appropriately. If no other code is listening for the event, then nothing happens, but at least the Summary view can say that it’s done its job.
Here’s how we can generate a custom event in our view. We’re calling the event select
to indicate that the user has selected a specific run, and we’re passing the identifier of that run as a parameter associated with the event. At this point the Summary view is complete.
1 2 3 4 5 6 |
|
The component that should respond to this custom event is the same component that created the Summary view in the first place; that’s our app router. We’ll first need to listen for the event. We can do that right after we create it in the summary
method.
1 2 3 4 5 6 7 8 9 10 |
|
When the user selects a specific run from the summary view, Backbone.js calls our router’s selected
method. That method will receive any event data as parameters. In our case the event data is the unique identifier, so that becomes the method’s parameter.
1 2 3 4 |
|
As you can see, the event handler code is quite simple. It constructs a URL that corresponds to the Details view ('runs/' + id
) and passes that URL to the router’s own navigate
method. That method updates the browser’s navigation history. The second parameter ({ trigger: true }
) tells Backbone.js to also act as if the user had actually navigated to the URL. Because we’ve set up the details
method to respond to URLs of the form runs/:id
, Backbone.js will call details
and our router will show the details for the selected run.
When users are looking at a Detailed view, we’d also like to provide a button to let them easily navigate to the Summary view. As with the Summary view, we can add an event handler for the button and trigger a custom event when a user clicks it.
1 2 3 4 5 6 7 |
|
And, of course, we need to listen for that custom event in our router.
1 2 3 4 5 6 7 8 9 |
|
Once again we respond to the user by constructing an appropriate URL and triggering a navigation to it.
You might be wondering why we have to explicitly trigger the navigation change. Shouldn’t that be the default behavior? Although that may seem reasonable in most cases it wouldn’t be appropriate. Our application is simple enough that triggering the route works fine. More complex applications, however, probably want to take different actions depending on whether the user performs an action within the app or navigates directly to a particular URL. It’s better to have different code handling each of those cases. In the first case the app would still want to update the browser’s history, but it wouldn’t want to trigger a full navigation action.
Step 4: Fine-Tuning the Application
At this point our app is completely functional. Our users can view their summaries, bookmark and share details of specific runs, and navigate the app using the browser’s back and forward buttons. Before we can call it complete, however, there’s one last bit of housekeeping for us. The app’s performance isn’t optimal, and, even more critically, it leaks memory, using small amounts of the browser’s memory without ever releasing them.
The most obvious problem is in the router’s summary
method, reproduced below.
1 2 3 4 5 6 7 8 9 10 |
|
Every time this method executes it creates a new collection, fetches that collection, and renders a Summary view for the collection. Clearly we have to go through those steps the first time the method executes, but there is no need to repeat them later. Neither the collection nor its view will have changed if the user selects a specific run and then returns to the summary. Let’s add a check to the method so that we only take those steps if the view doesn’t already exist.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We can also add a check in the details
method. When that method executes and a Summary view is present, we can “set aside” the summary view’s markup using jQuery’s detach
function. That will keep the markup and its event handlers ready for a quick re-insertion onto the page should the user return to the summary.
1 2 3 4 5 6 7 8 9 10 11 |
|
Those changes make switching to and from the summary view more efficient. We can also make some similar changes for the details view. In the details
method we don’t have to fetch the run if it’s already present in the collection. We can add a check and, if the data for the run is already available, we won’t bother with the fetch.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
In the summary
method we don’t want to simply set aside the details view as we did for the summary view. That’s because there may be hundreds of detail views hanging around if a user starts looking at all of the runs available. Instead, we want to cleanly delete the details view. That lets the browser know that it can release any memory that the view is consuming. As you can see from the code below, we’ll do that in three steps.
- Remove the event handler we added to the details view to catch
summarize
events. - Call the view’s
remove
method so it releases any memory it’s holding internally. - Set
this.detailsView
tonull
to indicate that the view no longer exists.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
And with that change our application is complete. You can take a look at the complete result in the book’s source code.
Summing Up
In this appendix we’ve seen how to build an entire web application based on data and data visualizations. To help organize and coordinate our application we based it on the Backbone.js library, and we relied on the Yeoman tool to create the application’s scaffolding and boilerplate code and templates. Backbone.js lets us separate our application into models and views so that the code responsible for managing the data doesn’t have to worry about how that data is presented (and vice versa). It also gives us the flexibility to interact with REST APIs that don’t quite follow the normal conventions. The final Backbone.js component that we considered, the router, provided the support we needed to make sure our single page application behaves like a full web site so that our users can interact with it just as they would expect.