The Practical Client

Binding a TypeScript ViewModel to HTML Using Knockout

It's great building objects in TypeScript, but it isn't much good unless you can tie those objects into a Web page. Here's how to integrate TypeScript with Knockout (and a warning about where test driven development seems to stop).

In this column, I'm finally going to deliver what I've been building toward for the last few months: An HTML page in the client that uses TypeScript to integrate Web API services with a client-side ViewModel. Over the last few columns I've built a client-side ViewModel in TypeScript and some Web API services. In the column on Web Services, I used TypeScript with jQuery and JSON to call a method on the service and get back a list of customer objects. In this column I'll integrate a TypeScript ViewModel using Knockout to create a page that displays that list of customers in a dropdown list and then updates a textbox with data from the object selected in the dropdown list (see Patrick Steele's article for more about Knockout.)

Before writing any code, I use NuGet to add to my Test and Web project both the Knockout JavaScript library and the DefinitelyTyped Knockout file that supports working with Knockout in TypeScript. The type definitions in the DefinitelyTyped file give me IntelliSense support when writing code and allow the TypeScript compiler to check that I'm using the Knockout library correctly.

To get the design-time support for Knockout, I drag the DefinitelyTyped file from Solution Explorer over to my ViewModel code file in Visual Studio's editor window. When I drop the file, Visual Studio adds a TypeScript reference like this one:

/// <reference path="typings/knockout/knockout.d.ts" />

I'll now get IntelliSense support for Knockout (and some new Knockout datatypes for declaring properties) as I write my TypeScript code.

Integrating with Knockout
To use my ViewModel with Knockout to make the Customer objects I'm retrieving from my service available, I need to add a Knockout observable array to hold the Customer objects (later, I can bind that array to HTML elements in my user interface using Knockout's binding syntax). As I started writing this code, I decided that the interface I created earlier to define my ViewModel was adding work and not giving me much (anything) in return. So I deleted my ViewModel interface and just added a property to my ViewModel.

I call the property to hold my collection of Customers and declare it as a KnockoutObservableArray (as defined in the DefinitelyTyped file). Because I'm using TypeScript 9.1, I can use the generic version of the KnockoutObservableArray type and specify the kind of object that I'll be holding in the array (in this case, objects that implement my ICustomer interface, defined in my AdventureWorksEntities module).

It's not enough, however, to define a property in a class -- properties don't appear on a TypeScript object unless set to a value. The best way to handle this is to initialize the property in the ViewModel's constructor. I can add the property to my ViewModel and initialize it in one step by adding a public parameter to my ViewModel's constructor:

constructor(private cust: AdventureWorksEntities.ICustomer = null,
            private custs: AdventureWorksEntities.ICustomer[]= [],
            public customers: 
              KnockoutObservableArray<AdventureWorksEntities.ICustomer> = 
                            ko.observableArray([])) {}

I already have a method called fetchCustomers in my ViewModel that retrieves all of the Customer objects from a Web API service. Currently, that method just loads the retrieved collection into an internal field called custs (casting the collection to my ICustomer interface as it does so). I'll have that method load my observableArray also:

fetchAllCustomers()
{
  $.getJSON("http://localhost:49306/CustomerManagement",
            cs => {
                    this.custs = <AdventureWorksEntities.ICustomer[]>cs;
                    this.customers = ko.observableArray(this.custs);
                });                  
}

I want to display this collection in a dropdown list, showing the customer's last name. To make that happen with Knockout, I add a select tag to my page and use Knockout's binding syntax to tie the tag to my ViewModel customers property. That tag looks like this:

<select id="CustomerList" 
        data-bind="options: customers,  
                   optionsText: 'LastName'"/>

And here I made another decision. When I started this project, I defined interfaces and classes to represent the objects that my Web API services will be sending to me. In my code, however, I've only ever used the interfaces. I decided to delete my class definitions for my server-side objects and keep just their interfaces. Each of those interfaces defines properties that match the properties on my server side objects. My Customer interface, for example, looks like this:

export interface ICustomer 
{
  Id: number;
  FirstName: string;
  LastName: string;
  CustomerType: CustomersType;
}

After fetching the collection of ICustomer objects from my server, I need to call Knockout's applyBindings function, passing that function my ViewModel, to have Knockout display data from the ViewModel's customers property in the dropdown list. As a fan of test-driven development (TDD), I'd prefer to create a test that proves that I can successfully bind my data to the tag before creating the page.

I may prefer to do that, but I wasn't able to get it to work. That may reflect that I'm doing something wrong, or that I'm working with beta software, or that I'm just asking too much from my testing tools (I'm using the Qunit library to give me the test functions I need and the Chutzpah Visual Studio add-in to run my code). If you're interested in that failed experiment, see this article's sidebar "Testing with HTML, jQuery, and Knockout in Chutzpah (or Not)."

Integrating with the Page
In my Web page, my first step is to add script references to the jQuery and Knockout libraries -- and to my own JavaScript libraries. I have to remember to use the .js files generated from my TypeScript files and not the .ts files that I used in my test code:

<script src="Scripts/jquery-2.0.3.js"></script>
<script src="Scripts/knockout-2.3.0.js"></script>
<script src="Scripts/AdventureWorksEntities.js"></script>
<script src="Scripts/SalesOrderMvvm.js"></script>

Since I created an actual HTML page, I decided to extend the Knockout bindings in my dropdown list's data-bind attribute. I first extend my optionsText binding with a function that assembles the Customer's FirstName and LastName properties. I also add an optionsValue binding that specifies that the Id property on my Customer object is to be used in the option elements generated by Knockout. Finally, I add a value binding to have a property called id on my ViewModel updated with the value from the currently selected option in the dropdown list:

<select id='CustomerList' 
        data-bind="options: customers, 
        optionsText: function (cust) {
              return cust.FirstName + ' ' + cust.LastName},
        optionsValue: 'Id', 
        value: id "/>

I also add a textbox and bind it to that same id property on the ViewModel:

<input id='CustId' type="text" data-bind="value: id"/>

My next step is to add that id property to my ViewModel and initialize it. Again, I do that by adding a public parameter to my ViewModel's constructor:

constructor(private cust: AdventureWorksEntities.ICustomer = null,
            private custs: AdventureWorksEntities.ICustomer[]= [],
            public customers: 
               KnockoutObservableArray<AdventureWorksEntities.ICustomer> =
                                               ko.observableArray([]),
            public id: KnockoutObservable<number> = ko.observable(0)){} 

The page's code to load the dropdown list and coordinate it with the textbox in my Web page is simple: instantiate the ViewModel, call the method that retrieves and loads the collection of Customers into the customers property, and bind Knockout to the ViewModel:

<script>

   $(function () 
   {
     var custVM;
     custVM = new SalesOrderMvvm.CustomerVM();
     custVM.fetchAllCustomers();
     ko.applyBindings(custVM);
   });

I press F5 and, when my page comes up, my dropdown list displays all of my customer's first and last names; when I change the selection in the dropdown list, my textbox displays the Id of the currently selected customer. Success!

Because I want a "pure" TypeScript solution, I rewrite my initial JavaScript code into TypeScript, put it into a .ts file and replace my script block with a reference to the generated JavaScript file. Here's the same code, in TypeScript:

/// <reference path="typings/knockout/knockout.d.ts" />
/// <reference path="SalesOrderMvvm.ts" />
/// <reference path="typings/jquery/jquery.d.ts" />

$(function () 
{
  var custVM: SalesOrderMvvm.CustomerVM;
  custVM = new SalesOrderMvvm.CustomerVM();
  custVM.fetchAllCustomers();
  ko.applyBindings(custVM);
});

While the failure of my Knockout test code could be the result of me doing something silly, I do have evidence that I'm working with tools still figuring out how to work together. After making a change to a test, for example, I often have to press Ctrl_S twice when saving my file. The first time I save my file, Visual Studio/Web Essentials/TypeScript conspire together to generate the JavaScript version of my code; the second time I save my file, Visual Studio Test and Chutzpah conspire together to find my test and list it in Visual Studio's Test Explorer window.

On the other hand, I now have a working page that does something almost useful and, more importantly, I have IntelliSense and TDD support through the whole process -- well, except for the TDD code. I'm still happy.

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

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

  • TypeScript Tops New JetBrains 'Language Promise Index'

    In its latest annual developer ecosystem report, JetBrains introduced a new "Language Promise Index" topped by Microsoft's TypeScript programming language.

Subscribe on YouTube