The Practical Client
Managing Your Page States with Backbone Routers
Peter turns the management of his single-page Backbone application over to Backbone itself by integrating Backbone Routers and Events. Plus: How to simplify your TypeScript code with longer namespaces.
In my last few columns, I've looked at building a Backbone application with TypeScript and shown how to create a client-side Backbone Model that corresponds to a Customer object on my server (which, in turn, is created from data managed by Entity Framework). To bridge the gap from my server to the client, I use a Web API Web Service that returns several Customer objects when synced with the Backbone Collection that manages my Customer Models. I pass that Backbone Collection of Customers to two Backbone Views that work together to generate an HTML dropdown list (one View is responsible for the <select> element while the other View is responsible for generating an <option> tag for each Customer in the Collection).
When my page is first loaded, however, I have quite a lot of code to get Backbone to retrieve the data, build the Models, populate the Collection with those Models and generate HTML from my Views. That code is shown in Listing 1.
Listing 1: Explicitly Initiliazing Backbone
import som = require("SalesOrderModels");
import sov = require( "SalesOrderViews" );
import cms = som.CustomerModels;
import cvs = sov.CustomerViews;
import bb = Backbone;
$(function ()
{
var cl: cms.CustomerShortList;
cl = new cms.CustomerShortList();
var cv: cvs.CustomersSelectView;
cv = new cvs.CustomersSelectView();
cv.collection = cl;
cv.$el = $("#Customers");
cl.fetch({
success: () => cv.render()
});
});
That's a lot of code that would be required, with some variation, on any page where I use Backbone -- I'd prefer to centralize that code so I didn't need to duplicate it through my application's pages. Fortunately, Backbone provides a more integrated way of coordinating Backbone functionality: Routers and Events. This column will start to show how to use those tools with TypeScript. But, first, the usual caveat for this column: Before starting this project, I updated all of my NuGet packages in Visual Studio 2010. This time, I got new versions of the type definition files for jQuery and Backbone, plus an updated version of Require.js (this is definitely not something I'd do in a real, ongoing project). If you're trying this at home, different versions of these NuGet packages could give you different results.
The Purpose of Routes
If you've used routing in ASP.NET, you'll find defining routing for a Backbone Router looks familiar. In Backbone, a route consists of two parts: a template and a function name. When provided a URL, Backbone scans the list of routes, looking for a template that matches the URL. When Backbone finds a match, it calls the function through an Event (using Events allows a Router to notify any Backbone object about finding the match). Backbone templates, like routes in ASP.NET, also let you extract parameter values from the URL and assign those values to names, which you can use in your methods (see "Defining Templates" at the end of this article for more information about templates).
When the user triggers a change to your page you can assign a URL to your page's current state and add that state to your browser's history. In a single page application, think of these URLs as internal "state URLs" rather than "page URLs" that cause you to get a new page from the server. If the user clicks the browser's Back button to retrieve one of these state URLs, the browser will navigate back to the current page's state rather than get a new version of the page.
For example, if the user selects customer A123 from a dropdown list on a page named CustomerInfo.html, you would, using Backbone and the Web API, fetch customer A123's information and display it. Backbone will let you add this new state of your page to the browser's history with a URL that might look something like www.CustomerInfo.html#Customer/A123. When the user selects another customer (customer B456, for example), you would add that state to the history as www.CustomerInfo.html#Customer/B456. If, while looking at customer B456, the user hits the browser's Back button, the browser will navigate back to the pervious state, www.CustomerInfo.html#Customer/A123.
Leveraging a Router
This page state functionality is provided through the Backbone Router class. To create a Router in TypeScript, I first use import statements to reference those TypeScript files I need at compile time but not at runtime (in this case, those are the files containing the definitions of my Models and Views, which I've separated from their actual implementations). The import statements also establish prefixes (cms and cvs, in this case) that I'll use to reference those definitions in my code:
import cms = som.CustomerModels;
import cvs = sov.CustomerViews;
My next step is to use import statements with the require method to tell Require to download to the browser those JavaScript files I need at runtime (the files with the JavaScript code that implements the definitions from my previous files):
import som = require("SalesOrderModels");
import sov = require("SalesOrderViews");
I also set up a prefix to refer to Backbone itself (I use standard <script> tags in my HTML page to download the Backbone script file so I don't use the require function here):
import bb = Backbone;
For more on the import statement see my column on structuring TypeScript modules.
I'm now ready to define my router by extending the Router class that comes with Backbone. I've decided to keep all of my routers in a file called CustomerRouters.ts and define this router in a separate module/namespace called CustomerRouters:
export module CustomerRouters
{
export class CustomerSalesOrderRouter extends bb.Router
{
In the constructor for my class, I load the routes that I want to use on my page. For this example, I define just two routes. The first route has a template matching to a URL consisting of a single literal ("startState"); I tie that route to a method named initializeList. The second template matches to a two-part URL, where the first part of the URL is the string "customer," while the second part is a parameter called cust (whose values must be prefixed with the letters "cid"). I tie that template to a method called getCustomer:
constructor()
{
this.routes = <any>{"startState" :"initializeList",
"customer/cid:cust":"getCustomer"
super();
}
The call to the super is required by TypeScript in the constructor for any class that extends another class. In Backbone that call must follow any routes you've set up.
Getting Work Done
I'm now ready to set up event handlers for the events raised by the Router. I move the code that loads my dropdown list of customer names out of my start page and into my initializeList event handler. For this example, I'll handle the event through a function added to the router.
Here's the code I added to my initializeList function (fired by the router matching a URL):
initializeList()
{
var cl: cms.CustomerShortList;
cl = new cms.CustomerShortList();
var cv: cvs.CustomersSelectView;
cv = new cvs.CustomersSelectView();
cv.collection = cl;
cv.$el = $( "#Customers" );
cl.fetch({success: () => cv.render()});
}
For now, in my getCustomer event handler, I'll just display the value of the parameter that the template extracts from the URL. To have that value passed to my method, I just have to give my getCustomer method a parameter with the same name as the parameter in the template:
getCustomer( cust: string )
{
alert(cust);
}
Next month, I'll call this method when the user selects a customer from my dropdown list and enhance the method to do something useful.
Triggering the Router
But the reason that I started down this route was to reduce the code required to initialize my page. In my application's default page, I can leverage the code that I've wrapped up in my Router by calling the Router's navigate method. Prior to calling navigate, however, I have to start the Backbone history processing with this code:
bb.history.start();
By default, the navigate method navigates to the required state URL, but it doesn't trigger the event associated with it. Fortunately, the navigate method accepts as its second parameter any object literal that corresponds to the Backbone NavigateOptions interface. That interface includes a trigger property that, when set to true, causes the navigate method to also raise the event associated with the URL and, as a result, execute the corresponding event handler.
Here's the code that instantiates my Router, starts the Backbone history processing and calls the navigate method, passing the appropriate URL ("startState") and NavigateOptions:
import sor = require( "SalesOrderRouters" );
import bb = Backbone;
$(function ()
{
var rtr: sor.CustomerRouters.CustomerSalesOrderRouter;
rtr = new sor.CustomerRouters.CustomerSalesOrderRouter;
bb.history.start();
rtr.navigate("startState", {trigger: true});
});
Using the navigate method also adds the resulting URL to my browser's history list (unless I suppress it). The address box for the browser now says http://www.phvis.com/CustomerInfo.html#startState. If I navigate to another site, either by typing in a new URL or selecting a bookmark, when I hit the browser's Back button I return to http://www.phvis.com/CustomerInfo.html#startState just as I left it and without re-executing my initializeList function.
By the way: There are all sorts of good reasons why, when using the navigate method, you shouldn't trigger the processing associated with the URL. However, it makes sense (to me, at least) to use the trigger option when navigating to the "first state" of my application.
This is much simpler code for starting off my application than the original version. However, TypeScript will let me cut down on some of the verbiage in this code even further by extending my sor prefix to include my CustomerRouters namespace, like this:
import sr = require( "SalesOrderRouters" );
import sor = sr.CustomerRouters;
import bb = Backbone;
$(function ()
{
var rtr: sor.CustomerSalesOrderRouter;
rtr = new sor.CustomerSalesOrderRouter;
bb.history.start();
rtr.navigate("startState", {trigger: true});
});
Next month, I'll start doing something more useful than just displaying a dropdown list by further integrating Routers and Events: When the user selects a customer from the list, my page will display information for that customer. It's baby steps, people.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.