Site Meter
 
 

Knockout-ES5: a plugin to simplify your syntax

Knockout-ES5 is a plugin for Knockout.js that lets you use simpler and more natural syntax in your model code and bindings. For example, you can replace this:

var latestOrder = this.orders()[this.orders().length - 1]; // Read a value
latestOrder.isShipped(true);                               // Write a value

… with this:

var latestOrder = this.orders[this.orders.length - 1];     // Read a value
latestOrder.isShipped = true;                              // Write a value

… while still retaining all of Knockout’s capabilities of automatically refreshing your UI and automatically detecting dependencies between different model properties.

Basically, it’s all the goodness of Knockout, but without having to remember parentheses. It requires a moderately up-to-date browser (more about this later).

Getting started

Download knockout-es5.min.js and add a <script> reference after you reference Knockout itself, e.g.:

<script src='knockout-2.2.1.js'></script>
<script src='knockout-es5.min.js'></script>

Then you can declare model classes without needing any explicit references to ko.observable, e.g.:

function OrderLine(data) {
    this.item = data.item;
    this.price = data.price;
    this.quantity = data.quantity;
 
    this.getSubtotal = function() {
        return "$" + (this.price * this.quantity).toFixed(2);
    }
 
    // Instead of declaring ko.observable properties, we just have one call to ko.track 
    ko.track(this);
}

Notice that the properties are just plain properties, without being wrapped in ko.observable. This means that during computations, such as in getSubtotal, there’s no need to invoke them as functions to read their value (e.g., this.quantity()). Similarly if you wanted to change a value, you’d just use a plain old assignment, e.g.:

someOrderLine.quantity += 1;

… instead of function calls (e.g., someOrderLine.quantity(someOrderLine.quantity() + 1);).

How it works

It’s so very simple. ko.track walks the list of properties on your model object, and for each one, replaces it with an ES5 getter/setter pair that reads/writes a hidden underlying observable (initialised to the existing property value).

The neat thing is that the Knockout.js core library doesn’t need to know anything about this: when you get/set one of these properties, some observable is read or written, and therefore all of KO’s existing binding, computed, and dependency detection functionality just works perfectly. Knockout-ES5 doesn’t have to patch any of the KO internals, so it works with any recent version of KO.

Controlling which properties are upgraded

If you want to restrict which properties are upgraded to observability, pass an array of property names:

ko.track(someModelObject, ['firstName', 'lastName', 'email']);

By design, ko.track does not recurse into child objects. I would encourage you to declare child objects as instances of some class of your own, with its constructor having its own ko.track call — this gives you far more control over how much of the object graph is walked.

Accessing the observables

If you want to access the underlying ko.observable for a given property, e.g., so that you can subscribe to receive notifications when it changes, use ko.getObservable:

ko.getObservable(someModel, 'email').subscribe(function(newValue) {
    console.log('The new email address is ' + newValue);
});

About arrays

Array-valued properties are special. As well as upgrading them to support observability like other properties, Knockout-ES5 intercepts calls to push, pop, splice, etc., to trigger change notifications just as you’d expect. This means that any UI based on the array, such as a list, will update automatically if you add or remove items.

With Knockout-ES5, you can access the array’s subproperties directly, e.g.:

var numItems = myArray.length; // Don't have to write myArray().length

Also, Knockout-ES5 adds a few additional functions that have proven useful on ko.observableArray instances: remove, removeAll, destroy, destroyAll, replace.

Computed properties

The most important feature of Knockout.js, and what differentiates it from most other Model-View JavaScript libraries, is its “reactive” dependency-detection capabilities: its ability to chain computed properties, so that changes propagate through an arbitrary object graph into your UI, without needing you to declare those dependencies anywhere.

Traditionally, Knockout uses ko.computed for this. So how does it work in Knockout-ES5? There are a couple of different patterns you can choose from:

  1. Just put a plain function on your model. In the example at the top of this blog post, getSubtotal is a plain old function. By invoking it from a binding, KO will detect the function’s dependencies (in this case, price and quantity) and will automatically refresh the UI when either changes:

    <span data-bind="text: getSubtotal()"></span>

    Pretty straightforward.

  2. Use ko.defineProperty. This is provided by Knockout-ES5 and is a KO-style equivalent to Object.defineProperty. It lets you declare a computed property with a get (and optionally set) function, for example:

    ko.defineProperty(this, 'subtotal', function() {
        return this.price * this.quantity;
    });
     
    // Alternatively, the third arg can be an object like { get: function() { ... }, set: ... }

    The advantage of this is that (A) you can read the subtotal property without having to invoke it as a function (as in getSubtotal()), and (B) its value will be cached and reused for all future invocations until a dependency changes, instead of re-running your get logic for each evaluation:

    <span data-bind="text: subtotal"></span>

    To ensure dependencies can be detected, place ko.defineProperty calls after your ko.track call. Or, if you want to put ko.defineProperty first, make sure no other code tries to evaluate the computed property before ko.track runs (if it evaluates before dependencies are tracked, it won’t be able to detect those dependencies).

Browser support

Knockout-ES5 works in ECMAScript 5-capable browsers. Let’s consider whether that’s appropriate for your project.

Since the Age of Antiquity, browsers such as IE6 have supported the ECMAScript 3 (ES3) JavaScript specification. It’s a tired old workhorse of a spec. Since then, all modern browsers have moved to ECMAScript 5 (ES5). You’ve been running ES5 for some years already, at least since IE9/Firefox 4/Chrome 6. ES5 adds a wealth of language and runtime primitives that open up valuable new possibilities. It’s no surprise that, when deciding which older IE versions to leave behind, jQuery 2.0 chose to support only IE 9 and newer, where ES5 is available.

Of course, Knockout.js itself takes backward compatibility very seriously: it has 100% support for anything from IE6 and Firefox 2 onwards. You can drop it into pretty much any web app with confidence. That is not changing. But today there are many projects where you know for sure your code will run only in an ES5 environment, for example:

  • Large, sophisticated public web apps (such as the one I currently work on) that already require at least IE9 or another modern browser
  • Intranet applications for sane corporate environments
  • PhoneGap apps targetting iOS/Android/WP8
  • Server-side code running inside Node.js

Summary: Knockout.js itself continues to support ES3 browsers such as IE6, and Knockout-ES5 is an optional plugin for those projects where it’s safe to depend on ES5.

The source

If you’re have Knockout experience already and are interested in more details about the implementation of Knockout-ES5, see the source. It’s shorter than this blog post.

Animating lists with CSS 3 transitions

Ever wanted to implement animated lists or grids, like in the following 12-second video?

This may look like a native iOS app, but this UI is all implemented with HTML, CSS, and JavaScript (see the previous post for details about my new iPhone app, Touralot, built with PhoneGap). So, how does all this slidey stuff work?

Using “transform” and “transition” in lists

The basic principles here are:

  • For animations to be smooth and hardware-accelerated, they have to be done with transform: translate3d
  • Even so, element positions should still be determined by the DOM structure (minimizing use of JavaScript)

So, consider some markup like the following:

<ul class="items">
    <li>Monday</li>
    <li>Tuesday</li>
    <li>Wednesday</li>
    ...
</ul>

The element positions are determined by the DOM structure, but not using translate3d. Now consider the following CSS rules:

.items { position: relative; }
.items li { 
    position: absolute; top: 0; left: 0;
    transition: all 0.2s ease-out;
}
 
.items li:nth-child(1)  { transform: translate3d(0, 0%, 0); }
.items li:nth-child(2)  { transform: translate3d(0, 100%, 0); }
.items li:nth-child(3)  { transform: translate3d(0, 200%, 0); }
...

As you can see, the <li> elements will now be positioned absolutely against the parent <ul>, and we use translate3d to recover the usual vertical offset based on each element’s position in the DOM. What’s more, there’s a transition rule so that, whenever translate3d changes, the browser smoothly animates the element to its new position.

[Aside: For Webkit, you’d need prefixed rules as well (-webkit-transform and -webkit-transition). Current versions of IE and Firefox don’t require the prefixes.]

What’s the result? Well, now whenever you change the set of <li> elements inside the <ul>, all the other <li> elements will smoothly animate into their new positions, without you having to write any code to tell them to do so. This works wonderfully if you’re generating your DOM through any kind of templates/binding library such as with Knockout’s foreach binding, but also works nicely just with plain old DOM operations. Try this:

The add and remove functionality doesn’t have to tell the elements to animate. They just do that naturally. For example, the code for add is just:

$(".append").click(function () {
    $("<li>New item</li>").insertAfter($(".items").children()[2]);
});

Browser quirks

Webkit (or at least Chrome) has annoying bug whereby it doesn’t account for in-flight CSS transitions when computing the scroll extents of a container. So you’ll need to do something like the following to force it to recompute the scroll container bounds at the end of the transition:

// Workaround for Webkit bug: force scroll height to be recomputed after the transition ends, not only when it starts
$(".items").on("webkitTransitionEnd", function () {
    $(this).hide().offset();
    $(this).show();
});

Also, since IE<10 doesn’t support transform or transition, you might also want to include some conditional CSS like the following to get basic non-animated behaviour on older browsers:

<!--[if lte IE 9]><style type="text/css">
    /* Old IE doesn't support CSS transform or transitions */
    .list-example .items li { position: relative; display: inline-block; }
</style><![endif]-->

It works for grids, too

With this technique, there’s nothing special about one-dimensional vertical lists. It works exactly as well for two-dimensional grids, without any changes.

Well actually, this raises the question of where all those .items li:nth-child(x) rules are coming from. So far I assumed you were writing them by hand. But how many such rules should you write? Couldn’t we generate them programmatically? Why yes, of course. Try this utility function:

function createListStyles(rulePattern, rows, cols) {
    var rules = [], index = 0;
    for (var rowIndex = 0; rowIndex < rows; rowIndex++) {
        for (var colIndex = 0; colIndex < cols; colIndex++) {
            var x = (colIndex * 100) + "%",
                y = (rowIndex * 100) + "%",
                transforms = "{ -webkit-transform: translate3d(" + x + ", " + y + ", 0); transform: translate3d(" + x + ", " + y + ", 0); }";
            rules.push(rulePattern.replace("{0}", ++index) + transforms);
        }
    }
    var headElem = document.getElementsByTagName("head")[0],
        styleElem = $("<style>").attr("type", "text/css").appendTo(headElem)[0];
    if (styleElem.styleSheet) {
        styleElem.styleSheet.cssText = rules.join("\n");
    } else {
        styleElem.textContent = rules.join("\n");
    }
}

You can specify a maximum number of elements to account for, and the number of columns desired in your grid (or pass 1 for a single-column vertical list). Of course, it would be nicer still if we could write just one rule that specified the translate3d values as a function of the element index, but I’m not aware of any way of doing that in CSS 3. Let me know if you can think of one!

Here’s the result, as a three-column grid:

Supporting arbitrary reorderings

The technique so far is great for animating all elements other than the one you’re inserting or reordering. But if you remove and insert an element in a new position, it will appear there instantly, without a transition, because it is a brand new element as far as the CSS transition logic is concerned.

So, how would it be possible to achieve something like the following, where the “reorder” button smoothly moves elements to new positions? Try it: click the random order button:

One possible technique is to override the translate3d values for each element with a snapshot of their current values, so that the elements retain their coordinates independently of their DOM order. Then, after mutating the DOM, remove your snapshot values, and then the CSS transition will kick in to move the element smoothly to its final location.

Here’s are a couple of handle jQuery utility functions:

(function () {
    var stylesToSnapshot = ["transform", "-webkit-transform"];
 
    $.fn.snapshotStyles = function () {
        if (window.getComputedStyle) {
            $(this).each(function () {
                for (var i = 0; i < stylesToSnapshot.length; i++)
                    this.style[stylesToSnapshot[i]] = getComputedStyle(this)[stylesToSnapshot[i]];
            });
        }
        return this;
    };
 
    $.fn.releaseSnapshot = function () {
        $(this).each(function () {
            this.offsetHeight; // Force position to be recomputed before transition starts
            for (var i = 0; i < stylesToSnapshot.length; i++)
                this.style[stylesToSnapshot[i]] = "";
        });
    };
})();

Now you can achieve the random reordering thing as follows:

$(".reorder").click(function () {
    $(".items li")
        .snapshotStyles()
        .tsort({ order: "rand" })
        .releaseSnapshot();
});

This works just the same with two-dimensional grids as with vertical lists.

For convenience, I used the tinysort library here (I only just learned about it this morning – it’s very neat), which can also sort the elements in a meaningful way, not only randomly. But it would work exactly the same if you write manual code to reorder the elements.

Supporting drag-and-drop reordering

Getting to this point may have seemed complicated, but it has some great benefits. Since the animation and positioning is controlled entirely by CSS, it composes beautifully with many other techniques for modifying the DOM. For example, if a drag-drop library shuffles the DOM elements, then they will now animate.

As an example, we can throw in jQuery UI’s sortable mechanism to enable drag-drop reordering. But let’s make it slick, and also animate the “dropping” phase of the operation, where the element you dragged moves from wherever you’re holding it into its final position.

Here’s how you can do that:

$("ul.items").sortable({
    start: function (event, ui) {
        // Temporarily move the dragged item to the end of the list so that it doesn't offset the items
        // below it (jQuery UI adds a 'placeholder' element which creates the desired offset during dragging)
        $(ui.item).appendTo(this).addClass("dragging");
    },
    stop: function (event, ui) {
        // jQuery UI instantly moves the element to its final position, but we want it to transition there.
        // So, first convert the final top/left position into a translate3d style override
        var newTranslation = "translate3d(" + ui.position.left + "px, " + ui.position.top + "px, 0)";
        $(ui.item).css("-webkit-transform", newTranslation)
                    .css("transform", newTranslation);
        // ... then remove that override within a snapshot so that it transitions.
        $(ui.item).snapshotStyles().removeClass("dragging").releaseSnapshot();
    }
});

Here’s the result. Be sure to try this in a desktop browser, not a phone/tablet, because jQuery UI sortable doesn’t handle touch events by default. Also if you’re on IE, be sure to use IE10+ or you won’t see animations.

Weird flicker in Chrome? It only happens when the example is in this iframe. Try the “Edit in JSFiddle” link to see it without.

… and here’s a two-dimensional grid, with the same drag-drop code:

If you want better support for phones and tables, there are various ways of upgrading jQuery UI “sortable” to respond to touch events. Or write your own.


Touralot: an iOS app built with PhoneGap, Knockout, and Azure Mobile Services

As a weekend project for the past few months, I’ve been building my first real mobile app: Touralot. Finally, it’s done, and it’s been approved on the iOS App Store!

Touralot lets you publish your own web-based guides to your favourite places to go and see. Maybe you’re off on vacation and would like to build a record of cool places you see, add photos, ratings, descriptions, plot them on a map, and then publish it all to the web. Or maybe you’re at home and are particularly knowledgeable about local restaurants, kids’ play centres, or whatever, and want to share your expertise.

This video gives a sense of the user experience:

Video looks blurry? Use a different browser, or open the video directly.

If you’re on an iPhone, you can get it now from the App Store (free).

The technology

Touralot is a Cordova/PhoneGap app, with the UI powered by Knockout.js, and a backend data service provided by Windows Azure Mobile Services (and a couple of other Azure facilities). It works entirely offline, except of course at the point where the user wants to log in and publish their stuff:

Screen Shot 2013-03-13 at 12.12.01

Why did I use HTML/JS and PhoneGap instead of native code (e.g., Objective-C)?

  • I wanted to see how far I could push HTML/JS to match the quality experience that people expect from native apps. This includes:
    • Standardised UI elements that respond intuitively to touch, drag/drop, and swipe gestures
    • Butter-smooth hardware-accelerated transitions
    • Retina-sharp professional artwork
    • Offline/syncing support

    Assess the result for yourself – I don’t imagine that it would even occur to normal users, at least on reasonably up-to-date devices (iPhone 4S/5), that this isn’t native.

  • This app can now be made cross-platform with relative ease. There’ll be some restyling effort involved, since Android and Windows Phone have different UI conventions, but hopefully it will mainly just be CSS changes.
  • I don’t know much Objective-C…

Coming up

Over the next few weeks I’m planning a series of blog posts to describe many of the techniques I used and challenges I went through while developing this app, along with recommendations and tips for PhoneGap development. Some of these techniques are well known, some might even be original…