KnockoutJS, WebAPI, and TypeScript

JavaScript is not Java

As Brent so famously once said "JavaScript is not Java". My reply was "Dynamic, loosely typed, AND case sensitive...yea, that'll work". I tried to ignore JavaScript, hoping it would go away. jQuery helped a lot, but it still felt loose and dirty. Somebody out there heard my complaints. My biggest complaint with JavaScript has always been that it didn't have any concept of types, and you got very poor build-time support from development environments.

Enter TypeScript

I believe that Anders Hejlsberg would have been one of the great contributors to software developers productivity just for TurboPascal, Delphi, or C#, or .NET, but his work to modernize without breaking JavaScript through TypeScript, I predict will be far more reaching than any of the others. Add to that the fact that Microsoft has made TypeScript open source, and that it has IDE support in Eclipse and Visual Studio (among others), and you have the makings of a perfect storm.

What is TypeScript?

TypeScript lets you write JavaScript the way you really want to. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open Source.

What about KnockoutJS and WebAPI?

KnockoutJS allows you to simply create bindings to HTML content. It is not as big as Angular or Ember, but does it's job. Combine it with a nice WebAPI back end and you have (IMHO) a nice entry into single page applications that you can apply incrementally. Lots of bang for little bucks. This blog is for things that I want to remember, not necessarily for external consumption. In this case, there are a couple of tricks to get TypeScript, Knockout, jQuery, and (to a lesser extent WebAPI) to work together seamlessly. Oh, and I threw in Bootstrap just because it is awesome.

If you'd like to jump to the end, you can access the complete visual studio project from my bit bucket repository.

So, let's get started.
  1. Hook up knockoutJS to a simple web page
  2. Use TypeScript as the knockout model
  3. Create the world's simplest WebAPI service
  4. Persist changes to the web API using jQuery
What?!?! too much to read? Ok. I'll skip to the good parts: We'll just talk about the TypeScript model and UI.
This is the guts of the HTML for our mini single page application (SPA). If you are familiar with Knockout, it will look very familiar to you. The first section is an "Add student area". The second section is our "List View", and the third is our "Details Dialog". The extra styling is all bootstrap. The script at the end of the page includes the JavaScript that gets included in the next section as well as some start up code that calls our web API, retrieves the initial list of items (students) and initializes the knockout view model from the resulting values.
<div class="row">
    <div class="col-sm-6 col-sm-offset-3">
        <form role="form" data-bind="submit: addItem">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <span class="panel-title">Add Student</span>
                    <button class="close" type="submit" data-bind="enable: givenNames().length > 0 && familyNames().length > 0"><span class="glyphicon glyphicon-plus"></span></button>
                </div>
                <div class="panel-body">
                    <div class="form-group">
                        <input type="text" class="form-control" placeholder="given names" data-bind='value: givenNames, valueUpdate: "afterkeydown"' />
                    </div>
                    <div class="form-group">
                        <input type="text" class="form-control" placeholder="family names" data-bind='value: familyNames, valueUpdate: "afterkeydown"' />
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>

<div class="row">
    <div class="col-sm-6 col-sm-offset-3">
        <div class="panel panel-default">
            <div class="panel-heading"><span class="panel-title" data-bind="text: itemCount()"></span></div>
            <ul class="list-group" data-bind="foreach: items">
                <li class="list-group-item">
                    <span data-bind="text: GivenNames"></span> <span data-bind="    text: FamilyNames"></span>
                    <button type="button" class="close" aria-hidden="true" data-bind="click: $root.editItem" data-toggle="modal" data-target="#editItem"><span class="glyphicon glyphicon-pencil"></span></button>
                </li>
            </ul>
        </div>
    </div>
</div>

<div id="editItem" class="modal fade">
    <div class="modal-dialog modal-sm">
        <div class="modal-content" data-bind="with: editItem">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true" data-bind="click: function (data) { $root.getItem(data); }"><span class="glyphicon glyphicon-remove"></span></button>
                <h4 class="modal-title">Edit Student</h4>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="given names" data-bind='value: GivenNames, valueUpdate: "afterkeydown"' />
                </div>
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="family names" data-bind='value: FamilyNames, valueUpdate: "afterkeydown"' />
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal" data-bind="click: function (data) { $root.getItem(data); }">Close</button>
                <button type="button" class="btn btn-primary" data-dismiss="modal" data-bind="click: function (data) { $root.saveItem(data); }">Save</button>
                <button type="button" class="btn btn-warning" data-dismiss="modal" data-bind="click: function (data) { $root.deleteItem(data); }">Delete</button>
            </div>
        </div>
    </div>
</div>
@section scripts{
    <script type="text/javascript" src="~/Scripts/WebService.js"></script>

    <script type="text/javascript">
        $(function () {
            $.getJSON("/api/Student", null, function (data) {
                ko.applyBindings(new WebServiceModel(data));
            });
        });
    </script>
}

Ok, now lets look at the TypeScript code for the view model.
This is where it gets interesting. The apparent comments at the top of the file actually are imported references to the other libraries used in this script. We need jQuery, Knockout, and KnockoutMapping. I imported these using NuGet. Once I'd done that, I have many strong types available to me: KnockoutObservable instead of ko.observable(), for example. I have two different models here: the Student model, and the WebServiceModel as a View Model for the whole page.
The next interesting thing we come upon is the student map. This is a knockout option that allows us to use our student id as a key so that Knockout Mapping can keep track of new and updated entities.
Now for a really important part. My item count computed observable has an optional parameter: the owner property. This property permanently maps the this function to the class we are interested in without a manual closure.
The constructor is called at start up and initializes our objects to the items list.
The jquery AJAX calls work with their HTML bindings above to make sure we have a correct reference to the WebServiceModel when the calls return. The HTML is very important because it overrides the binding that Knockout would normally apply. Inside the success callbacks, we have two options, one is to use function notation and change the context to 'this'. Using this method, when the jQuery callback returns, it remembers that the WebServiceModel was the context for our call. Without it, we would get a jQuery selector as 'this'. The other way is to use a lambda function (due for inclusion in the next version of JavaScript). A lambda function is supposed to preserve the calling context when the call returns, removing the need to create a closure. However, when TypeScript compiles to javascript, a _this reference is introduced and used as a closure. This mechanism works fine for us as well, so we don't need to change the jQuery context to preserve our 'this'. Either way works fine, it is just a matter of taste. I've coded the 'deleteItem(item)' method using this style.
///
///
///

class Student {
    Id: KnockoutObservable;
    GivenNames: KnockoutObservable;
    FamilyNames: KnockoutObservable;
}

class WebServiceModel {
    private studentMap: KnockoutMappingOptions = {
        key: d => ko.utils.unwrapObservable(d.id)
    };
    items: KnockoutObservableArray = ko.observableArray([]);
    givenNames: KnockoutObservable = ko.observable("");
    familyNames: KnockoutObservable = ko.observable("");
    editItem: KnockoutObservable = ko.observable(null);
    itemCount: KnockoutComputed = ko.computed(
        {
            owner: this,
            read: function () {
                if (typeof this.items === "undefined") return "";
                switch (this.items().length) {
                    case 0:
                        return "The list is empty";
                    case 1:
                        return "There is only one student";
                    default:
                        return "There are " + this.items().length + " students";
                }
            }
        });

    constructor(json: any) {
        ko.mapping.fromJS(json, this.studentMap, this.items);
    }

    addItem() {
        $.ajax({
            context: this,
            type: "POST",
            url: '/api/Student',
            dataType: 'json',
            data: { GivenNames: this.givenNames(), FamilyNames: this.familyNames() },
            success: function (d) {
                this.givenNames("");
                this.familyNames("");
                this.items.push(ko.mapping.fromJS(d));
            }
        });
    }

    getItem(item) {
        $.ajax({
            context: this,
            dataType: "json",
            url: "/api/Student",
            data: null,
            success: function (d) {
                ko.mapping.fromJS(d, this.studentMap, this.items);
            }
        });
    }

    saveItem(item) {
        $.ajax({
            context: this,
            type: 'PUT',
            dataType: "json",
            url: '/api/Student/' + item.Id(),
            data: ko.mapping.toJS(item),
            success: function (d) {
                ko.mapping.fromJS(d, this.studentMap, this.items);
            }
        });
    }

    deleteItem(item) {
        $.ajax({
            url: '/api/Student/' + item.Id(),
            type: 'DELETE',
            success: d =>; {
                this.items.remove(item);
            }
        });
    }
}
There are going to be a few disconnects between some libraries, TypeScript introduces its own conventions for 'compiling' to JavaScript. The real issue here was a combination of TypeScript conventions, jQuery conventions, and Knockout bindings. By understanding what each library was trying to do to work around the dirty edges of JavaScript, we were able to create a simple pattern that blends the three nicely.

Comments

Anonymous said…
It would have been nice if the jquery part was done using type script.
Anonymous said…
Hi, I am busy learning typescript, and found your post. Found it interesting and would like to work through the code.
But can't find the link to the repository?
Thank you

Popular posts from this blog

Database Projects, SQL Unit Tests, and TeamCity

Building nice XML from SQL Server Tables

Brent: Programmer. Gamer. Cheapskate. All around good guy.