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:
- 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
andquantity
) and will automatically refresh the UI when either changes:
<div class="wp_syntax">
<div class="code">
<pre class="javascript"><span style="color: #339933;"><</span>span data<span style="color: #339933;">-</span>bind<span style="color: #339933;">=</span><span style="color: #3366CC;">"text: getSubtotal()"</span><span style="color: #339933;">></</span>span<span style="color: #339933;">></span></pre>
</div>
</div>
Pretty straightforward.
- 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 aget
(and optionallyset
) function, for example:
<div class="wp_syntax">
<div class="code">
<pre class="javascript">ko.<span style="color: #660066;">defineProperty</span><span style="color: #009900;">(</span><span style="color: #000066; font-weight: bold;">this</span><span style="color: #339933;">,</span> <span style="color: #3366CC;">'subtotal'</span><span style="color: #339933;">,</span> <span style="color: #003366; font-weight: bold;">function</span><span style="color: #009900;">(</span><span style="color: #009900;">)</span> <span style="color: #009900;">{</span>
<span style="color: #000066; font-weight: bold;">return</span> <span style="color: #000066; font-weight: bold;">this</span>.<span style="color: #660066;">price</span> <span style="color: #339933;">*</span> <span style="color: #000066; font-weight: bold;">this</span>.<span style="color: #660066;">quantity</span><span style="color: #339933;">;</span> <span style="color: #009900;">}</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #006600;">// Alternatively, the third arg can be an object like { get: function() { ... }, set: ... }</span></pre>
</div>
</div>
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:
<div class="wp_syntax">
<div class="code">
<pre class="javascript"><span style="color: #339933;"><</span>span data<span style="color: #339933;">-</span>bind<span style="color: #339933;">=</span><span style="color: #3366CC;">"text: subtotal"</span><span style="color: #339933;">></</span>span<span style="color: #339933;">></span></pre>
</div>
</div>
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.