In this section, we will use D3 and Backbone to create a single page application to display a time series of stock prices. The user will be able to select different stocks and a period of time in order to get a detail view. The page will have several components:
We will display the control and title view on top of the page, a big area with the detail view, and a context area chart at the bottom of the page.
As you can see, there are several components that should be in sync. If we change the stock, the area chart should be updated. If we change the time interval in the context chart, the detail chart must be updated to show you only the selected period. We will use Backbone to structure this application.
The state of our application can be described by the stock and the time interval that we want to examine. We will create a model to store the application state and one view for each component.
The general strategy is that each view will contain an instance of a D3-based chart, which is created following the reusable chart pattern. In the initialize
method, we will tell the view to listen for changes in the model and invoke the render
method when one of the model attributes is changed. In the render
method of the view, we will create a selection for the container element of the view, bind the data corresponding to the selected stock, and invoke the charting
function using the selection.call
method. We will begin by creating reusable charts with D3.
In this section, we will implement the charts that will be used by the application. This time, the code of the charts will be in a separated JavaScript file. To follow the examples in this section, open the chapter06/stocks/js/lib/stockcharts.js
and chapter06/01-charts.html
files.
We will begin by creating the stock title chart and then implement the stock area chart, which we will be using in the context and detail views. Note that the title chart is not really necessary, but it will be helpful to introduce the pattern that integrates reusable charts and Backbone.
The stock title chart is a reusable chart that creates a paragraph with the title of the stock. As we mentioned previously, it's probably not a good idea to create a chart just to write a string, but it shows you how to integrate a reusable chart that doesn't involve SVG with Backbone. It has a configurable title accessor function, so the user can define the content of the paragraph using the data that is bound to the container selection. The chart is structured using the reusable chart pattern, as shown in the following code:
function stockTitleChart() { 'use strict'; // Default title accessor var title = function(d) { return d.title; }; // Charting function function chart(selection) { selection.each(function(data) { // Creation and update of the paragraph... }); } // Title function accessor chart.title = function(titleAccessor) { if (!arguments.length) { return title; } title = titleAccessor; return chart; }; return chart; }
In the
charting
function, we select the div
element and create a selection for the paragraph. We add the stock-title
class to the paragraph in order to allow the user to modify its style, as shown in the following code:
// Charting function function chart(selection) { selection.each(function(data) { // Create the selection for the title var div = d3.select(this), par = div.selectAll('p.stock-title').data([data]); // Create the paragraph element on enter par.enter().append('p') .attr('class', 'stock-title')); // Update the paragraph content par.text(title); }); }
As usual, we can use the chart by creating and configuring a chart instance, selecting the container element, binding the data, and invoking the chart using the selection.call
method, as shown in the following code:
// Create and configure the title chart var titleChart = stockTitleChart() .title(function(d) { return d.name; }); // Select the container element, bind the data and invoke // the charting function on the selection d3.select('div#chart') .data([{name: 'Apple Inc.'}]) .call(titleChart);
The stock area chart will display the time series for the stock price as an area chart. In Chapter 5, Creating User Interface Elements, we implemented an area chart that uses the brush behavior to select a time interval and annotate the chart with additional information about the price variation in the period. We will create an improved version of this chart and use it in the stock explorer application.
Besides having the usual width, height, and margin attributes and accessors methods, this chart will have an optional axis, brush behavior, and a configurable brush listener function so that the user can define actions to be performed on the brush. The time extent can be also be configured, allowing the user to show only part of the chart.
We have added all these methods so that we can use two chart instances for different purposes: one to allow the user to select a time interval and another to display the selected time interval in more detail. In the chapter06/01-charts.html
file, we created one instance in order to select the time interval:
var contextAreaChart = stockAreaChart() .height(60) .value(function(d) { return d.price; }) .yaxis(false) .onBrushListener(function(extent) { console.log(extent); });
We will use the chart accessor methods to set the height, disable the y axis, and set the value accessor and the brush listener functions. In this case, the brush listener function will display the time extent in the browser console on brush.
We will use a second instance of the same chart to display a specific time interval. In this instance, we will disable the brush control and set the initial time extent, the value, and the date accessors. This chart will display the stock prices between the from
and to
dates:
// Set the time extent var from = new Date('2002/01/01'), to = new Date('2004/12/31'), // Create and configure the detail area chart var detailAreaChart = stockAreaChart() .value(function(d) { return d.price; }) .date(function(d) { return new Date(d.date); }) .timeExtent([from, to]) .brush(false);
As you have probably guessed, the first instance is intended to control the time extent of the second chart instance. We will get to that soon; in the meantime, we will discuss some implications of controlling the time extent of the chart.
If we change the time extent of the chart, we will want the chart to reflect its new state. If the brush is dragged left in the first chart, we will want the area of the second chart to move to the right-hand side until it matches the interval selected in the first chart, and if we shorten the time interval in the first chart, we will want the area of the second chart to compress itself to display the selected interval in the same horizontal space.
The stock area chart will be implemented as a reusable chart. As most of the chart structure is similar to the chart presented in the previous section, we will skip some parts for brevity:
function stockAreaChart() { 'use strict'; // Chart Attributes var width = 700, height = 300, margin = {top: 20, right: 20, bottom: 20, left: 20}; // Time Extent var timeExtent; // The axis and brush are enabled by default var yaxis = true, xaxis = true, brush = true; // Default accessor functions var date = function(d) { return new Date(d.date); }; var value = function(d) { return +d.value; }; // Default brush listener var onBrush = function(extent) {}; function chart(selection) { selection.each(function(data) { // Charting function contents... }); } var svgInit = function(selection) { ... }; // Accessor Methods... return chart; }
In order to have the detail chart moving in sync with the context chart, we will need to draw the complete series in the detail chart but only displaying the interval selected with the brush in the context chart. To prevent the area chart from being visible outside the charting area, we will define a clip path, and only the content inside the clipping path will be visible:
var svgInit = function(selection) { // Define the clipping path selection.append('defs') .append('clipPath') .attr('id', 'clip') .append('rect') .attr('width', width - margin.left - margin.right) .attr('height', height - margin.top - margin.bottom); // Create the chart and axis groups... };
The element that will be clipped should reference the clipping path using the clip-path
attribute. In the charting
function, we select the container element and create the SVG element on enter. We also set the SVG element's width and height and translate the axis, chart, and brush groups. We create the scales and axis (if they are enabled) and create and configure the area generator to draw the chart area path:
// Charting function... // Add the axes if (xaxis) { svg.select('g.xaxis').call(xAxis); } if (yaxis) { svg.select('g.yaxis').call(yAxis); } // Area Generator var area = d3.svg.area() .x(function(d) { return xScale(date(d)); }) .y0(yScale(0)) .y1(function(d) { return yScale(value(d)); });
We create a selection for the path and bind the time series array to the selection. We append the path on enter and set its class to stock-area
. We set the path data using the area generator and set the clip-path
attribute using the clipPath
variable defined previously:
// Create the path selection var path = svg.select('g.chart').selectAll('path') .data([data]); // Append the path element on enter path.enter().append('path') .attr('class', 'stock-area'), // Set the path data string and clip the area path.attr('d', area) .attr('clip-path', 'url(#clip)'),
We create an envelope brush listener function. In this function, we retrieve the brush extent and invoke the user-configurable onBrush
function, passing the extent as an argument. We initialize the brush behavior and bind the brushListener
function to the brush event:
// Brush Listener Function function brushListener() { timeExtent = d3.event.target.extent(); onBrush(timeExtent); } // Brush Behavior var brushBehavior = d3.svg.brush() .x(xScale) .on('brush', brushListener);
The initial time extent of the chart can be configured. If that's the case, we update the brush behavior extent, so the brush overlay fits the configured time extent:
// Set the brush extent to the time extent if (timeExtent) { brushBehavior.extent(timeExtent); }
We call the brush behavior using the selection.call
method on the brush group and set the overlay height:
if (brush) { svg.select('g.brush').call(brushBehavior); // Change the height of the brushing rectangle svg.select('g.brush').selectAll('rect') .attr('height', h); }
The preceding charts are implemented following the reusable chart pattern and were created with D3 only. We will use the charts in a Backbone application, but they can be used in other applications as standalone charts.
In Backbone projects, it is a common practice to create directories for the models, views, collections, and routers. In the Chapter06
directory, we created the stocks
directory to hold the files for this application:
stocks/ css/ js/ views/ models/ collections/ routers/ lib/ app.js data/ index.html
The models
, views
, collections
, and routers
folders contain JavaScript files that contain the Backbone models, views, collections, and routers. We add the D3 charts to the js/lib
directory; additional JavaScript libraries would be there too. There is also a data
folder with JSON files for the stock data. The index.html
file contains the application markup.
In the header of the page, we include style sheets and JavaScript libraries that we need for our application. To create the page more quickly, we will use a CSS library that will add styles to enable uniform fonts, sizes, and default colors among browsers and define the grid system. A grid system is a set of styles that allows us to define rows and columns of standard column sizes without having to define the styles for each size ourselves. We will use Yahoo's Pure CSS modules to use the grid system, which is a pretty minimal set of CSS modules. These modules are used only in this page; if you are more comfortable with Bootstrap or other libraries, you are free to replace the div
classes or define the sizes and behaviors of each container yourself.
We will create a container for the application and add the pure-g-r
class, which is a container with responsive behavior enabled. If the viewport is wide, the columns will be shown side by side; if the user has a small screen, the columns will be shown stacked. We will also create two child containers, one for the stock control and title and a second container for the stock area chart, both classed pure-u-1
, that is, containers with full width. The pure
container uses fractional sizes to define the div width; in order to have a div that covers 80 percent of the parent container width, we can set its class to pure-u-4-5
:
<div class="pure-g-r" id="stock-app"> <!-- Stock Selector and Title --> <div class="pure-u-1"> <div id="stock-control"></div> <div id="stock-title"></div> </div> <div class="pure-u-1 charts"> <div id="stock-detail"></div> <div id="stock-context"></div> </div> </div>
We include the application files at the end of the page so that the markup is rendered while the remaining assets are loaded:
<!-- Application Components --> <script src="/chapter06/stocks/js/models/app.js"></script> <script src="/chapter06/stocks/js/models/stock.js"></script> <script src="/chapter06/stocks/js/collections/stocks.js"></script> <script src="/chapter06/stocks/js/views/stocks.js"></script> <script src="/chapter06/stocks/js/views/app.js"></script> <script src="/chapter06/stocks/js/routers/router.js"></script> <script src="/chapter06/stocks/js/app.js"></script>
Models contain application data and the logic related to this data. For our application, we will need a model to represent the stock information and the application model, which will store the visualization state. We will also create a collection that holds the available stock instances. To avoid polluting the global namespace, we will encapsulate the application components in the app
variable:
var app = app || {};
Adding this line to all the files in the application will allow us to extend the object with models, collections, and views.
The stock model will contain basic information about each stock. It will contain the stock name (Apple Inc.), the symbol (AAPL), and the URL where the time series of prices can be retrieved (aapl.json
). Models are created by extending Backbone.Model
:
// Stock Information Model app.Stock = Backbone.Model.extend({ // Default stock symbol, name and url defaults: {symbol: null, name: null, url: null}, // The stock symbol is unique, it can be used as ID idAttribute: 'symbol' });
Here, we defined the default values for the model to null
. This is not really necessary, but it might be useful to know which properties are expected. We will use the stock symbol as ID
. Besides this, we don't need any further initialization code. As stock symbols are unique, we will use the symbol as the ID for easier retrieval later. We can create stock instances by using the constructor and setting the attributes that pass an object:
var appl = new app.Stock({ symbol: 'AAPL', name: 'Apple', url: 'aapl.json' });
We can set or get its attributes using the accessor methods:
aapl.set('name', 'Apple Inc.'), aapl.get('name'), // Apple Inc.
In this application, we will create and access stock instances using a collection rather than creating individual instances.
To define a collection, we need to specify the model. When defining the collection, we can set the URL of an endpoint where the collection records can be retrieved, which is usually the URL of a REST endpoint. In our case, the URL points towards a static JSON file that contains the stocks records:
// Stock Collection app.StockList = Backbone.Collection.extend({ model: app.Stock, url: '/chapter06/stocks/data/stocks.json' });
Individual stocks can be added to the collection one by one, or they can be fetched from the server using the collection URL. We can also specify the URL when creating the collection instance:
// Create a StockList instance var stockList = new app.StockList({}); // Add one element to the collection stockList.add({ symbol: 'AAPL', name: 'Apple Inc.', url: 'aapl.json' }); stockList.length; // 1
As we defined the stock symbol as idAttribute
, individual stock instances can be retrieved using the stock's ID. In this case, the stock symbol is the ID of the stock model, so we can retrieve stock instances using the symbol:
var aapl = stockList.get('AAPL'),
Models use the URL of the collection to construct their own URL. The default URL will have the form collectionUrl/modelId
. If the server provides a RESTful API, this URL can be used to create, update, and delete records.
We will create an application model to store and manage the application state.
To define the application model, we extend the Backbone.Model
object, adding the corresponding default values. The stock
attribute will contain the current stock symbol (AAPL), and the data will contain the time series for the current stock:
// Application Model app.StockAppModel = Backbone.Model.extend({ // Model default values defaults: { stock: null, from: null, to: null, data: [] }, initialize: function() { this.on('change:stock', this.fetchData); this.listenTo(app.Stocks, 'reset', this.fetchData); }, // Additional methods... getStock: function() {...}, fetchData: function() {...} });
We will also set a template for the stock collection data. In this case, the base URL is chapter06/stocks/data/
. As we mentioned previously, there is a JSON file in the data
directory with the data of the available stocks:
// Compiled template for the stock data url urlTemplate: _.template('/chapter06/stocks/data/<%= url %>'),
We have also added a fetchData
method in order to retrieve the time series for the corresponding stock. We invoke the template that passes the current stock data and use the parsed URL to retrieve the stock time series. We use the d3.json
method to get the stock data and set the model data attribute to notify the views that the data is ready:
fetchData: function() { // Fetch the current stock data var that = this, stock = this.getStock(), url = this.urlTpl(stock.toJSON()); d3.json(url, function(error, data) { if (error) { return error; } that.set('data', data.values); }); }
To integrate the D3-based charts with Backbone Views, we will use the following strategy:
The views for the page components are in the chapter06/stocks/js/views/stocks.js
file, and the application view code is in the chapter06/stocks/js/views/app.js
file.
This view will simply display the stock symbol and name. It's intended to be used as a title of the visualization. We create and configure an instance of the underlying chart and store a reference to the chart in the chart
attribute. In the initialize
method, we tell the view to invoke the render method when the model's stock
attribute is updated.
In the render
method, we create a selection that will hold the container element of the view, bind this element to a dataset that contains the current stock, and invoke the chart using selection.chart
:
app.StockTitleView = Backbone.View.extend({ chart: stockTitleChart() .title(function(d) { return _.template('<%= symbol %><%= name %>', d); }), initialize: function() { this.listenTo(this.model, 'change:stock', this.render); this.render(); }, render: function() { d3.select(this.el) .data([this.model.getStock().toJSON()]) .call(this.chart); return this; } });
Changes to the stock
attribute of the application model will trigger the change:stock
event, causing the view to invoke its render
method, updating the D3 chart. In this particular view, using a reusable chart is overkill; for a real-life problem, we could have used a small Backbone View with a template. We did this to have a minimal example of reusable charts working with Backbone Views.
To add some diversity, we will create the selector without using D3. This view will show you the available stocks as a selection menu, updating the application model's stock
attribute when the user selects a value. In this view, we will use a template. To create a template, we create a script element of type text/template in the index.html
file and assign it an ID, in our case, stock-selector-tpl
:
<script type="text/template" id="stock-selector-tpl"> <select id="stock-selector"> ... </select> </script>
Underscore templates can render variables using <%= name %>
and execute JavaScript code using <% var a = 1; %>
. Here, for instance, we evaluate the callback
function on each element of the stocks
array:
<!-- Create the stocks selector and add its options --> <% _.each(stocks, function(s) { %> <option value="<%= s.symbol %>"><%= s.symbol %></option> <% }); %>
For each one of the elements of the stocks
array, we add an option with the stock symbol attribute as the value and content of the option element. After rendering the template with the application data, the HTML markup will be as follows:
<select id="stock-selector"> <option value="AAPL">AAPL</option> <option value="MSFT">MSFT</option> <option value="IBM">IBM</option> <option value="AMZN">AMZN</option> </select>
In the Backbone View, we select the content of the script with the stock-selector-tpl
ID, compile the template to use it later, and store a reference to the compiled template in the template attribute:
// Stock Selector View app.StockSelectorView = Backbone.View.extend({ // Compiles the view template template: _.template($('#stock-selector-tpl').html()), // DOM Event Listeners events: { 'change #stock-selector': 'stockSelected' }, // Initialization and render methods... });
We set the events
attribute, with maps' DOM events of the inner elements of the view, to methods of the view. In this case, we bind changes to the stock-selector
select element (the user changing the stock) with the stockSelected
method.
In the initialize
method, we tell the view to render when the app.Stocks
collection emits the reset
event. This event is triggered when new data is fetched (with the {reset: true}
option) and when an explicit reset is triggered. If a new set of stocks is retrieved, we will want to update the available options. We also listen for changes to the application model's stock
attribute. The current stock should always be the selected option in the select element:
initialize: function() { // Listen for changes to the collection and the model this.listenTo(app.Stocks, 'reset', this.render); this.listenTo(this.model, 'change:stock', this.render); this.render(); }
In the render
method, we select the container element and set the element content to the rendered template, writing the necessary markup to display the drop-down control. We pass a JavaScript object with the stock
attribute set to an array that contains the app.Stocks
model's data. Finally, we iterate through the options in order to mark the option that matches the current stock symbol as selected:
render: function() { // Stores a reference to the 'this context' var self = this; // Render the select element this.$el.html(this.template({stocks: app.Stocks.toJSON()}); // Update the selected option $('#stock-selector option').each(function() { this.selected = (this.value === self.model.get('stock')); }); }
Backbone models and collection instances have the JSON method, which transforms the model or collection attributes to a JavaScript object. This method can be overloaded if we need to add computed properties besides the existing attributes. Note that in the each
callback, the this
context is set to the current DOM element, that is, the option element. We store a reference to the this
context in the render
function (the self variable) in order to reference it later.
The context view contains a small area chart that allows the user to select a time interval that can be displayed in the detail view:
We will use the same strategy as the one used in the previous views to create and configure an instance of stockAreaChart
, and store a reference to it in the chart
attribute of the view:
app.StockContextView = Backbone.View.extend({ // Initialize the stock area chart chart: stockAreaChart() .height(60) .margin({top: 5, right: 5, bottom: 20, left: 30}) .date(function(d) { return new Date(d.date); }) .value(function(d) { return +d.price; }) .yaxis(false), // Render the view on model changes initialize: function() { ... }, render: function(e) { ... } });
In the initialize
method, we tell the view to listen for changes to the application model and set the chart brush listener to update the from
and to
attributes of the model:
initialize: function() { // Get the width of the container element var width = parseInt(d3.select(this.el).style('width'), 10); // Bind the brush listener function. The listener will update // the model time interval var self = this; this.chart .width(width) .brushListener(function(extent) { self.model.set({from: extent[0], to: extent[1]}); }); // The view will render on changes to the model this.listenTo(this.model, 'change', this.render); },
We get the width of the this.el
container element using D3 and set the chart width. This will make the chart use the full width of the user's viewport.
The render
method will update the chart's time extent, so it reflects the current state of the model, creates a selection that holds the container element of the view, binds the stock data, and invokes the chart using selection.call
:
render: function() { // Update the time extent this.chart .timeExtent([ this.model.get('from'), this.model.get('to') ]); // Select the container element and call the chart d3.select(this.el) .data([this.model.get('data')]) .call(this.chart); return this; }
This view is the only component that can change the from
and to
attributes of the model.
The stock detail view will contain a stock area chart, showing only a given time interval. It's designed to follow the time interval selected in the stock context view.
We create and configure a stockAreaChart
instance, setting the margin, value, and date accessors and disabling the brushing behavior:
// Stock Detail Chart app.StockDetailView = Backbone.View.extend({ // Initialize the stock area chart chart: stockAreaChart() .margin({top: 5, right: 5, bottom: 30, left: 30}) .value(function(d) { return +d.price; }) .date(function(d) { return new Date(d.date); }) .brush(false), // Render the view on model changes initialize: function() { ... }, render: function() { ... } });
As we did in the context view, we tell the view to invoke the render
method on model changes in the initialize
method:
initialize: function() { // Get the width of the container element var width = parseInt(d3.select(this.el).style('width'), 10); // Set the chart width to fill the container this.chart.width(width); // The view will listen the application model for changes this.listenTo(this.model, 'change', this.render); },
In the render
method, we update the chart time extent, so the visible section of the area chart matches the time interval specified by the application model s from
and to
attribute:
render: function() { // Update the chart time extent var from = this.model.get('from'), to = this.model.get('to'), this.chart.timeExtent([from, to]); // Select the container element and create the chart d3.select(this.el) .data([this.model.get('data')]) .call(this.chart); }
Note that when using object.listenTo(other, 'event', callback)
, the this
context in the callback
function will be the object that listens for the events (object
).
The application view will be in charge of creating instances of the views for each component of the application.
The initialize
method binds the reset event of the app.Stocks
collection and then invokes the collection's fetch
method, passing the {reset: true}
option. The collection will request the data to the server using its url
attribute. When the data is completely loaded, it will trigger the reset
event, and the application view will invoke its render
method:
// Application View app.StockAppView = Backbone.View.extend({ // Listen to the collection reset event initialize: function() { this.listenTo(app.Stocks, 'reset', this.render); app.Stocks.fetch({reset: true}); }, render: function() { ... } });
In the
render
method, we create instances of the views that we just created for each component. At this point, the current symbol of the application can be undefined, so we get the first stock in the app.Stocks
collection and set the stock
attribute of the model if is not already set.
We proceed to initialize the views for the title, the selector, the context chart, and the detail chart, passing along a reference to the model instance and the DOM element where the views will be rendered:
render: function() { // Get the first stock in the collection var first = app.Stocks.first(); // Set the stock to the first item in the collection if (!this.model.get('stock')) { this.model.set('stock', first.get('symbol')); } // Create and initialize the title view var titleView = new app.StockTitleView({ model: this.model, el: 'div#stock-title' }); // Create and initialize the selector view var controlView = new app.StockSelectorView({ model: this.model, el: 'div#stock-control' }); // Create and initialize the context view var contextView = new app.StockContextView({ model: this.model, el: 'div#stock-context' }); // Create and initialize the detail view var detailView = new app.StockDetailView({ model: this.model, el: 'div#stock-detail' }); // Fetch the stock data. this.model.fetchData(); return this; }
Finally, we tell the model to fetch the stock data to allow the context and detail chart to be rendered. Remember that when the data is ready, the model will set its data
attribute, notifying the charts to update its contents.
In our application, the state of the visualization can be described by the stock symbol and the time interval selected in the context chart. In this section, we will connect the URL with the application state, allowing the user to navigate (using the back button of the browser) the bookmark and share a particular state of the application.
We define the routes for our application by assigning callbacks for each hash URL (Backbone provides support for real URLs too). Here, we define two routes, one to set the stock and one to set the complete state of the application. If the user types the #stock/AAPL
hash fragment, the setStock
method will be invoked, passing the 'AAPL'
string as the argument. The second route allows you to navigate to a specific state of the application using a URL fragment of the #stock/AAPL/from/Mon Dec 01 2003/to/Tue Mar 02 2010
form; this will invoke the setState
method of the router:
app.StockRouter = Backbone.Router.extend({ // Define the application routes routes: { 'stock/:stock': 'setStock', 'stock/:stock/from/:from/to/:to': 'setState' }, // Initialize and route callbacks... });
The router also has an initialize
method, which will be in charge of synchronizing changes in the application URL with changes in the application model. We will set the model for the router and configure the router to listen for change events of the model. At the beginning, the data might not have been loaded yet (and the from
and to
attributes might be undefined at this point); in this case, we set the stock symbol only. When the data finishes the loading, the from
and to
attributes will change and the router will invoke its setState
method:
// Listen to model changes to update the url route initialize: function(attributes) { this.model = attributes.model; this.listenTo(this.model, 'change', function(m) { if (m.get('from') && m.get('to')) { this.setState(m.get('stock'), m.get('from'), m.get('to')); } else { this.setStock(m.get('stock')); } }); },
The setStock
method updates the symbol
attribute of the model. The navigate
method updates the browser URL to reflect the change of stock. Note that we are using the time interval as a variable of the application state. If we select an interval, the back button of the browser will get us to the previously selected time intervals. This might not be desirable in some cases. The choice of which variables should be included in the URL will depend on the application and the behavior that most users will expect. In this case for instance, an alternative approach could be to update the application state on drag start and drag end, not on every change in the interval:
// Set the application stock and updates the url setStock: function(symbol) { var urlTpl = _.template('stock/<%= stock %>'), this.model.set({stock: symbol}); this.navigate(urlTpl({stock: symbol}), {trigger: true}); },
The setState
method parses the from
and to
parameters from the URL as dates and sets the model's stock
, from
, and to
attributes. As we cast the strings to the date, we can use any format recognizable by the date constructor (YYYY-MM-DD, for instance), but this will imply that we format the from
and to
attributes to this format when the model changes in order to update the URL. We will use the toDateString
method to keep things simple. After setting the model state, we construct the URL and invoke the navigate
method to update the browser URL:
// Set the application state and updates the url setState: function(symbol, from, to) { from = new Date(from), to = new Date(to); this.model.set({stock: symbol, from: from, to: to}); var urlTpl = _.template('stock/<%= stock %>/from/<%= from %>/to/<%= to %>'), fromString = from.toDateString(), toString = to.toDateString(); this.navigate(urlTpl({stock: symbol, from: fromString, to: toString}), {trigger: true}); }
The simple addition of a router can make an application way more useful, allowing users to bookmark and share a particular state of the page and navigate back to previous states of the application.
Once we have created the application models, collections, views, and router, we can create the instances for the application model and view. The application initialization code is in the chapter06/stocks/js/app.js
file.
We begin by creating an instance of the app.StockList
collection:
// Create an instance of the stocks collection app.Stocks = new app.StockList();
The collection instances will be retrieved later.
We create an instance of the application model and an instance of the application view. We initialize the application model by indicating the model of the view and the container element ID:
// Create the application model instance app.appModel = new app.StockAppModel(); // Create the application view app.appView = new app.StockAppView({ model: app.appModel, el: 'div#stock-app' });
Finally, we initialize the router, passing the application model as the first argument, and then we tell Backbone to begin monitoring changes to hashchange
events:
// Initializes the router var router = new app.StockRouter({model: app.appModel}); Backbone.history.start();