Practical TypeScript

Integrating JavaScript with TypeScript (and Backbone and Knockout)

Peter looks at how to call a JavaScript function from your TypeScript code and do it in a type-safe way. Along the way, he dramatically simplifies a Backbone application by integrating Knockout.

I have two topics for this month's column. Primarily, I want to show you how to integrate native JavaScript functions into your TypeScript application, and do it in a way that gives you IntelliSense support and compile-time checking for how that function is called. But the excuse I'm using to cover that topic is a project where I'll integrate Knockout into a Backbone application I've been building and, as a result, dramatically simplify the code required to get a grid of data displayed on a page. Specifically, I'm going to replace the one-way Backbone data binding with the two-way data binding that Knockout provides (facilitated by a third library called Knockback). But even if you're not interested in Backbone or Knockout, you should be able to leverage the integration of JavaScript and TypeScript.

Two warnings: If you've been using the downloads I've provided with previous columns in this series on Backbone with TypeScript, you'll have to abandon them with this installment. In addition to adding Knockout and Knockback to the project, I upgraded to the latest version of jQuery to support Knockback. The code in this month's installment won't work with the project's previous downloads. Also, because of the state that I'm leaving this project in, until the next column and download, all the application will do is retrieve one customer of your choice.

Why Knockout?
Some background: In "Creating a Master/Detail Page with Backbone and TypeScript" last month's column, using Backbone I created a master-detail page that allowed the user to select a single customer from a dropdown list and see all the information for the selected customer plus a table of all of the sales orders for that customer. If you read that column, you may have found the amount of procedural code that I had to put in the Backbone View a little daunting (if you didn't read the column, trust me: the code was daunting). Backbone uses events to pick up changes the user makes in the page and that can result in a lot of code.

Knockout, another JavaScript library, uses a different model than Backbone for getting data to and from the page. With Knockout you declaratively tie the properties on an object holding your data (a ViewModel) to elements on your page. Knockout then takes care of moving data seamlessly from the ViewModel to the page and, as the user makes changes, from the page back to the ViewModel.

And that's where the Knockback library comes in: Knockback includes a ViewModel class that, effectively, wraps around the Model objects that Backbone uses to hold your data. Backbone still takes care of managing your page state and moving data back and forth between the browser and your services. You just rewrite your Backbone Views to use Knockback and Knockout to get data on and off the page … and, in the process, drastically simplify your code. The Knockback wrapper moves the data out of your Backbone Models and, through Knockout, onto the page. Then, as the user makes changes to the page, the data flows back through Knockout to the ViewModel, which quietly updates your Backbone Models. It's a slick little system.

Integrating Knockout, Knockback and Backbone
The first step in integrating these packages is to use NuGet to add Knockout, Knockback and the DefinitelyTyped files that describe those libraries for TypeScript to my project. The next step is to define the Knockback ViewModel that extends my Backbone Model.

For my sample project, I created a single Knockback ViewModel I called CustomerVM that handles all the data on the page for a single customer. The CustomerVM ViewModel includes properties to hold the customer's Id, LastName, FirstName, CompanyName and a collection of SalesOrderHeaders (see "Creating a Master/Detail Page with Backbone and TypeScript " from last month for how I retrieve all of this information in a single trip to the server). My code begins with a reference to Knockback through its DefinitelyTyped definition. That reference lets me use the Knockback ViewModel as the basis for my class (I put this code in a file called SalesOrderViewModels.ts), as shown in Listing 1.

Listing 1: Using the Knockback ViewModel As the Basis for the Class
import kb = Knockback;

export module CustomerViewModels
{
  export class CustomerVM extends kb.ViewModel
  {
    Id: number;
    FirstName: string;
    LastName: string;
    CompanyName: string;
    SalesOrdersHeaders: kb.CollectionObservable;
  }
}

As you can see, my SalesOrderHeaders collection is defined as a Knockback collectionObservable type. The collectionObservable monitors changes made to each Model in the collection and passes the changes to Knockout to update the page; as Knockout responds to the user's changes to the page, collectionObservable finds the right Model in the collection and updates the right property on the Model with the data from the page.

However, this is just the class definition: I need to instantiate this class to use it in my code. I started to do that in TypeScript, but couldn't get my code to compile. Figuring out the problem would've been challenging: There aren't a lot of sites on the Web with sample Knockback/TypeScript code to help me.

Integrating JavaScript
What is on the Web, though, are lots of sites showing the required JavaScript code to create a KnockBack ViewModel … and using JavaScript gives me an opportunity to show how to integrate native JavaScript code into a TypeScript program.

My first step is to write a JavaScript function that returns a JavaScript version of my TypeScript CustomerVM ViewModel. It's essential that the property names in this JavaScript object match the property names on my TypeScript class exactly: same spelling and use of uppercase and lowercase (it was an interesting experience, by the way, giving up much of the IntelliSense support and compile-time checking that I've come to take for granted with TypeScript when writing this function).

In the JavaScript code, I use the Knockout observable class to define the simple properties (LastName, FirstName and so on) and the Knockback collectionObservable class to define my SalesOrderHeaders property. My function accepts two parameters: cust (which will be a Backbone Model holding customer data) and orders (which will be a Backbone Collection of SalesOrderHeader objects). In the JavaScript code you can't, of course, see the datatypes of the parameters or the return type of the function:

function CreateKnockBackViewModel(cust, orders) {
  var cvm = {
             Id: kb.observable(cust,'Id'),
             CompanyName: kb.observable(cust, 'CompanyName'),
             FirstName: kb.observable(cust, 'FirstName'),
             LastName: kb.observable(cust, 'LastName'),
             SalesOrderHeaders: kb.collectionObservable(orders)
           }
  return cvm;
}

I put this code in a file I named KnockbackInterface.js.

To call this function from TypeScript, I first need a TypeScript definition of the function. Because that definition is in TypeScript, I can specify datatypes for the input parameters and the function's return type. Thanks to that datatyping, when I get around to writing the code that calls the function, IntelliSense will prompt both for the right classes for the parameters and the variable that catches the result. Furthermore, if I attempt to use the wrong class, the compiler will spot the problem and flag the errors as I type my code in.

The TypeScript definition for my JavaScript function begins with two import statements that allow me to pick up definitions that I've created in my SalesOrderModels and SalesOrderViewModels files (see my first column on Backbone for the definition of those classes). I use the TypeScript require function to ensure those files get loaded into the browser at runtime and eliminate the need to provide script tags for those files (I covered the details of modularizing TypeScript applications in an earlier column). The next two import statements reference modules (the TypeScript equivalent of namespaces) that are defined inside those files and contain my class definitions:

import som = require("SalesOrderModels");
import sovm = require( "SalesOrderViewModels" );

import cms = som.CustomerModels;
import custvms = sovm.CustomerViewModels;

I'm now ready to define a type-safe TypeScript interface for my function's parameters and return type. I can give my interface any name I want, but I picked one that incorporates the function's name:

interface CreateKnockBackViewModelInterface
{
  (cust: cms.CustomerLong, orders: bb.Collection): custvms.CustomerVM;
}

Finally, I tell TypeScript a function called CreateKnockBackViewModel exists that implements this interface using the TypeScript declare statement. Again, I have to ensure the name I use here exactly matches the name of my JavaScript function in spelling and casing:

declare var CreateKnockBackViewModel:CreateKnockBackViewModelInterface;

In TypeScript talk this is called an "ambient declaration." I added my ambient declaration to the top of the file with the Backbone View that will call my JavaScript function (if I thought I might use the function in multiple places in my application, I'd put the ambient declaration in its own file and import it into those other places). Now that I have a TypeScript definition of my class and some JavaScript code that creates an instance of the class, I can write the code that will use this function as part of getting my Backbone data onto my page.

Integrating Backbone and Knockout
In my application, I'm already using a Backbone View to handle displaying the page. The code that generates the page's HTML is in that View's render method. Rather than restructure my application, I just delete the code in the render method and replace it with some Knockout/Knockback code.

The initial part of that code is identical to the "pure" Backbone code I first used. I access the Customer Model associated with this View from the Backbone Collection that it's held in (at this point in the application there's only one Customer Model in that collection). I then pull the Orders associated with that customer from the Customer's Orders property. Because the Knockback ViewModel wraps a Backbone collection, I then move those orders into a Backbone Collection set up to hold SalesOrderHeader objects using Backbone's reset method:

var cl: cms.CustomerLong;
cl = this.collection.first();

var Orders: Array<cms.SalesOrderHeader>;
Orders = cl.get( "Orders" );

var ol: cms.SalesOrderHeaderList;
ol = new cms.SalesOrderHeaderList();
ol.reset(Orders);

Now, I define a variable to hold my Knockback ViewModel and call my JavaScript function through its TypeScript definition, passing the Customer Model and the SalesOrderHeader Collection:

var cvm: custvms.CustomerVM
cvm = CreateKnockBackViewModel(cl, ol);

I now use Knockout to delete any previous data bindings within an element in my page by calling Knockout's cleanNode method; I then bind my Knockback ViewModel to that element with the Knockout applyBindings method. As is conventional in Backbone Views, I finish by returning a reference to the View itself:

ko.cleanNode( $("#CustomerDetail")[0] );
ko.applyBindings( cvm, $( "#CustomerDetail" )[0] );
return this;

Building the Page
Finally, of course, I need to set up my HTML page to work with this code. First, I need the script tags that reference the necessary JavaScript libraries along with the file holding my JavaScript function. The last tag in this example references require.js and specifies the start point for my application in my SalesOrderApp.js file (see my previous column on modularizing a TypeScript application for the details on using require):

<script src="Scripts/Libraries/jquery-2.1.1.js"></script>
<script src="Scripts/Libraries/underscore.js"></script>
<script src="Scripts/Libraries/backbone.js"></script>
<script src="Scripts/Libraries/knockout-3.2.0.js"></script>
<script src="Scripts/Libraries/knockback.js"></script>
<script src="Scripts/Application/KnockbackInterface.js"></script>
<script data-main="Scripts/Application/SalesOrderApp.js" 
  type="text/javascript" 
  src="Scripts/Libraries/require.js"></script>

Next, I define a div element to hold the elements that I will data bind with Knockout (this is the element that my Knockout cleanNode and applyBindings functions reference). To display the Customer information, I use the Knockout data-bind attribute in input elements and reference the properties on my ViewModel (here, again, spelling and casing must match the property names exactly):

<div id="CustomerDetail">
  <h2>Customer Information</h2>
            
  <label>Id: </label><input data-bind="value: Id" />
  <label>Company Name: </label><input data-bind="value: CompanyName" />
  <label>First Name: </label><input data-bind="value: FirstName" />
  <label>Last Name: </label><input data-bind="value: LastName" />
  <br />

For the SalesOrder collection, I use the Knockout foreach attribute to generate a table row for each SalesOrder. Within the row, I use the data-bind attribute again to display the properties on the SalesOrderHeader class:

  <h3>Orders</h3>
  <table>
    <tbody data-bind='foreach: SalesOrderHeaders'>
      <tr>
        <td><input type="text" data-bind='value: SalesOrderID' /></td>
        <td><input type="text" data-bind='value: Status' /></td>
        <td><input type="text" data-bind='value: OrderDate' /></td>
      </tr>
    </tbody>
  </table>
</div>

If you're interested in the details of data binding with Knockout and TypeScript, I've done a column on that, too.

The net result of integrating Backbone with Knockout/Knockback is a dramatic reduction in both the amount of code in the View (less than half the lines) and in the HTML on the page. If I'd used the standard Backbone pattern for retrieving the SalesOrders, there would've been even less code. I've got a much, much simpler application to maintain.

In the next Practical TypeScript column I'll finish up this series on Backbone by adding create, read, update and delete functionality to this page.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.