The Practical Client

Retrieving Data with Backbone and TypeScript

Peter creates an AJAX-enabled application using TypeScript with Backbone that talks to a Web API service. He also upgrades to the latest version of Backbone TypeScript support, causing several things to break -- but it does result in better, simpler code.

If you haven't read the earlier columns in this series on using TypeScript with Backbone, please don't feel obliged to review them now, unless you're interested in the details of the topics I covered: (structuring a Backbone application and rendering a page). As part of writing this column on integrating Backbone support for making REST/JSON Web service calls with TypeScript, I upgraded my project to the "latest and greatest" of everything.

In that process, I picked up the latest version of the definition files for Backbone, which now requires you to declare Backbone Views and Collections as generic methods and classes. That's obviously a good thing (More type safety! Better IntelliSense support!), but it also broke much of my code. So I'll be reviewing some of my client-side code from the earlier columns in order to show how Backbone and TypeScript work now.

On the Server
My first step in having my client-side code access a Web service is, obviously, to create the Web service. My service returns Customer data my Backbone application will display in a dropdown list. I don't want to return the whole Customer object when all I need for the dropdown list is customer Id and FirstName plus LastName, so I create a data transfer class to return just that information. This class includes a calculated property (FullName), which, as you'll see, simplifies the client-side code that generates my HTML:

public class CustomerShort
{
  public int Id { get; set; }
  public string FullName { get; set; }
}

I'm using the ASP.NET Web API to create my service (it's easier than anything else), so I use NuGet to add the Microsoft ASP.NET Web API 2.1 packages to my application. To establish the URL my client-side code should use to access my service, I add a Global.asax file to my project that I'll use to specify the route to my Web Services controller (which I call CustomerController). I specify the route in the file's Application_Start method using the code in Listing 1.

Listing 1: Specifying the Route in the Application_Start Method of the File
  protected void Application_Start(object sender, EventArgs e)
  {
    GlobalConfiguration.Configure(config =>
    {
      config.MapHttpAttributeRoutes();

      config.Routes.MapHttpRoute(
                    name: "CustomerApi",
                    routeTemplate: " CustomerService",
                    defaults: new {controller="Customer"}
    );
  });
}

With all that in place, I create my Web Service as a Web API controller that returns objects from the AdventureWorks database, using Entity Framework 6.1 and an .edmx file (again, easier than anything else), as shown in Listing 2.

Listing 2: Creating the Web Service As a Web API Controller
public class CustomerController : ApiController
{
  private AdventureWorksLTEntities ade;

  public CustomerController()
  {
    ade = new AdventureWorksLTEntities();
  }
   
  public IEnumerable<CustomerShort> Get()
  {
    return from c in ade.Customers
           select new CustomerShort
                   	{
                       Id = c.CustomerID,
			 FullName = c.LastName + ", " + c.FirstName
                   	};          
  }
}

Defining Models and Collections
I can now start working on my client-side code. In the file that holds my Blackbone Models, I set up a shortcut to Backbone and define the interface for my client-side class (which matches my server-side class):

Import bb = Backbone;

export module CustomerModels
{
  export interface ICustomerShort
  {
     Id: number;
     FullName: string;
  }

I follow that with the definition of the client-side class that implements the interface. To work with Backbone, my class must extend the Backbone Model class. As you can see in Listing 3, the code in each property of my class handles moving data in and out of the properties in the underlying Backbone Model class I'm extending (Backbone will take care of converting the JSON objects that my Web API service returns into my CustomerShort class).

Listing 3: Extending the Backbone Model Class
export class CustomerShort extends bb.Model implements ICustomerShort
{
  get Id(): number
  {
    return this.get('Id');
  }
  set Id(value:number)
  {
    this.set('Id', value);
  }
  get FullName(): string
  {
    return this.get( 'FullName' );
  }
  set FullName( value: string )
  {
    this.set( 'FullName', value );
  }
}

(One note: Your Model class (CustomerShort, in my case) can not have a constructor. TypeScript won't compile your code if there's a constructor in your Model class that requires parameters. If you have a constructor that requires no parameters, your TypeScript code will compile, but your application won't work. It's certainly possible that there's some clever code you could put in a constructor to make the application work, but I couldn't find any. In the end, I just deleted the constructor my CustomerShort class originally had. I guess there's some benefit here: Without the constructor, my code is simpler.)

Finally, I wrote the Collection that holds all my CustomerShort classes and that drives the Views that generate my HTML. Here, with the new Backbone definition files, my code does get simpler, and in a good way. My Collection must extend the Backbone Collection class, but now I can specify the kind of objects the Collection will hold as part of declaring the class. The only thing I have to do with the Collection class is set its url property (which tells the Collection what URL to use to get its objects): I set that the route I established in my Global.asax file. The Collection class code looks like this:

export class CustomerShortList extends bb.Collection<CustomerShort>
{
  url = 'CustomerService';
}

Now, when requested, the Collection will make a REST request to the service at the URL in the url property. When the service returns JSON objects, the Collection will convert those JSON objects into the object type specified in its declaration (CustomerShort, in this case) and populate itself with the results.

Defining Views
My Backbone Views (which are responsible for generating the HTML that goes into my Web page) saw the fewest changes. However, Views are now also defined as generics, so I specify the Model class they'll be working with when declaring them. That means the files holding my Views code begins like this (the CustomersSelectView generates the select element for my dropdown list):

import bb = Backbone;
import som = require("SalesOrderModels");
import cms = som.CustomerModels;

export module CustomerViews
{
  export class CustomersSelectView extends bb.View<cms.CustomerShort>
  {
    private elm: JQuery;

One other change: Backbone allows you to use jQuery to manipulate the HTML your View is generating by using the jQuery $ shortcut. In the previous version of my application I had to precede a call to jQuery with a reference to Backbone, but that's no longer necessary. Instead I can just use the jQuery $, as in the following code, which finds a select tag in the HTML that Backbone generates:

this.elm = $("select");

See my earlier column for the details on how I used two Backbone Views to generate a single HTML select element with its option elements. In this column, I'll just look at the changes I've made in generating the option elements inside the select element.

Generating HTML for Model Objects
I first declare the View that will generate the option elements by extending the Backbone View and specifying the model class that the View will use:

export class CustomerOptionView extends bb.View<cms.CustomerShort>
{

In Backbone, HTML is generated in the View's render method, which returns a Backbone view (that method is also, now, a generic):

render(): bb.View<cms.CustomerShort>
{

Within the render method I can create the HTML any way I want, but Backbone comes with the underscore library. It includes, among other useful features, a templating engine. Until I have a better reason, I might as well use underscore to generate my HTML.

I first define a variable that can hold an underscore template processor, then load that variable with a processor created using underscore's template method. I pass underscore's template method an HTML string containing placeholders (marked with <%=…%>) that refer to properties on my CustomerShort object. When I call the template processor it will slot the values from a CustomerShort's properties into the HTML and hand me back the HTML:

var templateProcessor: ( ...parms: any[] ) => string;
templateProcessor = _.template( '<option value="<%= Id %>"><%= FullName %></option>' );

Now that I have a template processor, I pass it the contents of my View's model property, which will have been set to one of the CustomerShort objects the Collection retrieved (again, see the earlier column for how that's done). The toJSON method returns a collection of the properties on a Backbone object that the template processor can search for matches to the placeholders in the template. This code uses toJSON to convert the CustomerShort object in the model property into a format that underscore can use:

var html: string;
html = templateProcessor( this.model.toJSON() );

Finally, I need to add the HTML to my page using the View's setElement property (later, I'll specify where on the page that element is). A Backbone convention is that the render method always returns a reference to the View that it's part of (this supports chaining methods), so I do that, as well:

   this.setElement( html );
   return this;
  }
}

I did make one final change: Keeping the HTML for my template in my code isn't a great idea, as the template should be in my Web page with the rest of the HTML the UI designers will want to work with. I altered my underscore code to look for a script tag with an id of dropdowntemplate, giving me this code:

templateProcessor = _.template( $("#dropdowntemplate").html());

Invoking Backbone
I'm now ready to get my dropdown list displayed on the page. In the TypeScript file loaded by the page, I first set an import shortcut to Backbone and use jQuery to specify a function that will be called when the page is fully loaded:

import bb = Backbone;

$(function ()
  {

In that function, I instantiate my Collection and my topmost View (the CustomersSelectView that calls my CustomerOptionView), storing both the Collection and the View in variables:

var cl: cms.CustomerShortList;
cl = new cms.CustomerShortList();
  
var cv: cvs.CustomersSelectView;
cv = new cvs.CustomersSelectView();

I then configure my View. First, I tie my View to my Collection by setting the View's collection property to the variable holding my Collection. Then I set the View's $el property to a jQuery selector that will find the element on the page where I want the generated HTML to be inserted:

cv.collection = cl;
cv.$el = $("#Customers");

I'm now ready to tell the Collection to retrieve the data from my service. I do that by calling the Collection's fetch method. Once the data's returned, I generate my HTML by calling the related View's render method. There's a wrinkle here, though: The fetch method is asynchronous. I can't simply call the render method after calling the fetch method -- the JSON objects probably won't have been returned to the client in time.

Fortunately, the fetch method accepts, as a parameter, a class that implements the CollectionFetchOptions interface. That interface allows you to specify a method to be run when the fetch method succeeds in retrieving objects. I could, I suppose, have defined my own CollectionFetchOptions class using the interface, but that seemed like too much work. Instead, I passed an object literal to the fetch method that duplicated the CollectionFetchOptions. Or, at least, duplicated the one part I was interested in: the succeed property that holds the code to be called when the objects come back. I set the succeed property to an arrow expression that calls the View's render method:

    CustomersList.fetch({
                            success: () => cv.render()
                        });
    
});

This loose approach to data typing ("duck typing") is something that TypeScript supports: If an object looks enough alike to whatever's expected, that's OK with TypeScript.

I'm using RequireJS to manage downloading all of the JavaScript files generated from my TypeScript code (see my first column for the details on that). As a result, my HTML page, with the script tags for my libraries and my template, looks like Listing 4.

Listing 4: The HTML Page with Script Tags for Libraries and Template
<body>
  <form>
  <h1>Sales Orders</h1>
    Select a Customer: <div id="Customers"></div>
  </form>

  <script src="Scripts/Libraries/jquery-1.7.2.min.js"></script>
  <script src="Scripts/Libraries/underscore.js"></script>
  <script src="Scripts/Libraries/backbone.min.js"></script>
  <script data-main="Scripts/Application/SalesOrderApp.js" 
          type="text/javascript" 
          src="Scripts/Libraries/require.js"></script>

    <script type="text/template" id="dropdowntemplate">
      <option value="<%= Id %>"><%= FullName %></option> 
    </script>
</body>

I've included a download containing this Visual Studio 2012 project as part of this month's column. As my experience with this column suggests, it's entirely possible that, by the time you use the code in the download, one or other of the components I'm using will have changed, invalidating some part of my code. Don't blame me. More important: Don't call me. Well, unless you're offering money. Then you can call me.

Next month: Backbone Routers that will change the way that this code is invoked.

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/.

comments powered by Disqus

Featured

Subscribe on YouTube