The Practical Client

Responding to Events with TypeScript and Backbone

Peter upgrades his Backbone/Typescript to respond to the event raised when the user selects an item in a dropdown list by retrieving related data from a Web API service.

In previous columns here and here, I've used TypeScript and Backbone to build a single-page Web application that displays a list of customer names from the AdventureWorks database in a dropdown list, pulling the customer names from an ASP.NET Web API service (see "The Web API Service" for a quick walk-through). However, those previous columns have just created a read-only page. In this column, I'll show how to respond to a user selecting a customer from the dropdown list and display the related customer data. When I display that data, I'll also move the page to a new state that the browser will treat as if it were a different page.

Be aware: If you're new to Backbone, you might find the number of objects involved daunting. Backbone segregates code into Models (which hold data), Collections (which hold the Models retrieved from the service and handle communication with the service), and Routers (which capture URLs before your browser sees them and call functions in your Backbone objects). Last month's column concentrated almost exclusively on integrating Routers. This column takes an end-to-end look at combining all of these objects into one activity.

And here's the usual note for this series: When I started this phase of the project, I updated my NuGet packages to the latest and greatest of everything. If you're using the download I provided in last month's column, it's possible you might find incompatibilities when you add code from this column.

Revising the Page
In my previous columns, I had Backbone create a dropdown list by adding both the select and option elements to the page from code. In retrospect, I've decided it's a better design to include the select element in the page so I can make changes to the element without rewriting my code (changing that tag's class attribute, for example). So my new page consists of a div tag (called Customers) that defines the area Backbone will manage and, inside that my select tag and a div element to hold the information for a single customer. Here's the resulting HTML:

<body>
  <form>
    <h1>Sales Orders</h1>
    Select a Customer: <div id="Customers" >
                         <select id="CustomerSelect"></select>
  <h2>Customer Information</h2>

  <div id="CustomerDetail"></div>
                       </div>
  </form>

In my Backbone code, the CustomersSelectView (which handles populating the dropdown list) changes accordingly: In its render method, rather than adding a select element to my div element, I just look for the select element. Because Backbone lets me use jQuery to search for elements inside the element a View is tied to, I just need the code here:

export module CustomerViews
{
  export class CustomersSelectView extends bb.View<cms.CustomerShort>
  {
    private elm: JQuery;
          
    render(): bb.View<cms.CustomerShort>
    {
      this.elm = $("#CustomerSelect");
      ...rest of code...

Catching an Event
However, now I want to capture the change event fired by that CustomerSelect select tag when the user selects a customer from the list. To capture an event in Backbone you set the View's events property in the View's constructor (in addition to making the mandatory call to the super function that TypeScript requires in constructors). The events property is set to a list of event names and jQuery selectors (for specifying the element whose event you're capturing), tied to the name of the function to execute when the event is raised. This code, still in the CustomerSelectView that creates the dropdown list, first ties the View to my Customers div element and then links the change event for the CustomerSelect element inside that div element to a function called customerSelected:

constructor()
{
  this.setElement( $( "#Customers" ), true );
  this.events = <any> {"change #CustomerSelect": "customerSelected"};
  super();      
}

I've already instantiated my Router as part of starting up the application so it's wired itself into my browser (see the end of last month's column for that code). To generate a new state for my page, I just have to tell the browser to go to a URL that my Router will understand. The following code specifies a URL that my Router will recognize and then tacks the currently selected value for the dropdown list to the end of the URL:

customerSelected()
{            
  location.href = "#/customer/detail/" + this.elm.val();
}

With the View complete, it's time to look at what code the Router needs.

Routing the URL
To have my Router recognize this URL and take the appropriate action, I set the Router's routes property in its constructor. Here's the code to have the Router call a method named getCustomer when the Router is asked to navigate to a URL that matches the specified template (the route I used earlier to retrieve customers). To match my URL I use a template that matches to any URL beginning with the string "customer/detail" and holding a third parameter, which I've called custId (also shown here is the template I set up in last month's column which I use to initialize the application):

export module CustomerRouters
{
  export class CustomerRouter extends bb.Router
  {
    constructor()
    {
      this.routes = <any>{"customer/detail/:custId": "getCustomer",
                           "startState": "initializeList"}
      super();
    }

Because I added the current value of the dropdown list to the URL following "customer/detail", that custId parameter specified in my template will be set to that value.

The next step is to write the getCustomer method that I've tied to the template. The method will automatically be passed the custId parameter defined in the template. At the start of the getCustomer method I create the Backbone View that handles displaying all the data for a customer. I tie the View, through its $el property (which accepts jQuery queries), to the Customers div element in my page:

getCustomer(custId: number) 
{
  var clv: cvs.CustomerLongView;
  clv = new cvs.CustomerLongView();
  clv.$el = $( "#Customers" );

Still in my getCustomer method, I instantiate the Backbone Collection that will handle communication with my Web API service and hold all the customers retrieved from it (though, in this case, there should be only one customer retrieved). When I instantiate that Collection, I pass the custId value the collection should send to the service:

var cll: cms.CustomerLongList;
cll = new cms.CustomerLongList(custId);

I then attach the collection to my View and call the collection's fetch method. Calling the View's fetch method triggers a trip to my Web API service to retrieve my Customer object. If that trip succeeds, I call the View's render method to display the result in my page:

clv.collection = cll;
cll.fetch( {
            success: () => clv.render()
            });

Defining Models and Collections
Before looking at the View, let's look at the Model and Collection that hold the customer information returned from the service. Currently, I have two customer-related Models in my application: one for building the dropdown list (that Model has only two properties) and one for displaying all the information for a customer (that Model has a lot more properties). The only thing the two Models have in common is that both have an Id property. To support that design, I define a base CustomerDTO interface that contains just the customer Id property:

export module CustomerModels
{
  export interface ICustomerDTO
  {
    Id: number;
  }

I then define an interface for my long customer Model that extends my base interface with the additional properties I want to display:

export interface ICustomerLong extends ICustomerDTO
{
  LastName: string;
  FirstName: string;
  CompanyName: string;
  ...more properties...
}

With that interface defined, I use it to define a Model to actually hold the data (see Listing 1). I'll use the interface in my code wherever I have to declare a variable that will hold a CustomerLong object.

Listing 1: A Backbone Model
export class CustomerLong extends bb.Model implements ICustomerLong
{
  get Id(): number
  {
    return this.get( 'Id' );
  }
  set Id( value: number )
  {
    this.set( 'Id', value );
  }

  get FirstName(): string
  {
    return this.get( 'FirstName' );
  }
  set FirstName( value: string )
  {
    this.set( 'FirstName', value );
  }
  ...more properties...
}

I also need to define the collection that will hold the CustomerLong objects returned from my service and handle communication with the service. Both tasks are easy to do: In my Collection's declaration, I tie the Collection to my Model; In the Collection's constructor, I set the base class's URL property to the URL to be used when calling my Web API service. I set up the Collection's constructor to accept the Id of the customer to retrieve and include that in the URL used with the service:

export class CustomerLongList extends bb.Collection<CustomerLong>
{
  constructor( public CustId: number )
  {
    super();
    this.url = 'CustomerService/long/' + CustId;
  } 

Displaying Data with the View
All that's left is to define the View to be used to display the CustomerLong Model to the user. Before writing the View's code, I add a template to my HTML page to display the customer information:

<script type="text/template" id="customerdetailtemplate">
  First Name:   <input type="text" value="<%= FirstName %>" />
  Last Name:    <input type="text" value="<%= LastName %>" />
  Company Name: <input type="text" value="<%= CompanyName %>" />
</script>

In my View, I create an Underscore template processor and tie it to the template in my page:

export class CustomerLongView extends bb.View<cms.CustomerLong>
{
  render(): bb.View<cms.CustomerLong>
  {
    var templateProcessor: ( ...parms: any[] ) => string;
    templateProcessor = _.template( $( "#customerdetailtemplate" ).html() );

Next, I set up a string to hold the text that the template processor returns. I then call the template processor passing the first (and only) object received from my service (converted to JSON) and get my new text:

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

The next step is have the View find the div element where I want to insert my new text, again using the ability of Backbone to search inside the element that the View is tied to with jQuery:

var elm: JQuery
elm =  $("#Customers");

For the final step, I clear out any data already in the div element I just found and then append my new text to the element (and, as is conventional with Backbone Views, return a reference to the View):

    elm.html("");
    elm.append(html);
    return this;        
  }
}

That's an end-to-end look at catching an event and turning it into data on the page. With the customer data now being displayed, the next step is obvious: Allow the customer to update it. That's for next month.

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

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube