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.

41 Responses to Knockout-ES5: a plugin to simplify your syntax

  1. This looks cool Steve, thanks for providing it. I was literally thinking about this the other day.

    I have a question about browser support: do you have any plans to phase out IE6? Is it possible you could write ko in such a way that IE6 support can be added in as a plugin? Kind of the inverse of what you’re doing here where legacy support is an add on, rather than modern support is an add-on? IE6 usage is so low now, I can imagine the code base could be cleaned up quite a bit by dropping it?

  2. Rob

    Great job Steve! I had also started work on something very similar as a durandal plugin and have been using it on my own projects for a month or so with great success. I’m looking forward to digging through your implementation and comparing it to what I’ve got. Durandal users will be very happy about this :)

  3. Josh Olson

    Looks nice; unfortunately, I have to support MSIE8 for my day job.

    Curious — how does this work with the mapping plugin? I’m assuming it doesn’t. I gotta admit, I don’t like the fact ko.track doesn’t recurse/offer the ability to recurse. I like to send complex JSON from the server right into the mapping plugin… and if I need to override that process, I use a custom mapping. So this appears like I gain some functionality but lose a lot of niceties the mapping plugin provided.

    Also, I agree with Nick. It seems backwards — I would rather see Knockout stripped of legacy support and added back in via a plugin. What’s the plan going forward?

  4. Steve

    Agree on the comments here…

    http://www.w3schools.com/browsers/browsers_explorer.asp

    IE8 is still used more than IE9… wondering if this can be shimmed like the way ‘bind’ is shimmed in knockout ?

  5. Steve

    > do you have any plans to phase out IE6? Is it possible you could write ko in such a way that IE6 support can be added in as a plugin?

    I understand why that seems to make sense, but in terms of implementation, it’s probably not desirable. What it takes to support IE6-8 is a reasonably modest amount of special tricks scattered around in disconnected parts of the code. Taking this out would shrink the library a little, but to reintroduce it as a plugin would necessitate a whole bunch of new extensibility points in the core whose only reasonable use case is for enabling a “legacy browsers” plugin. Overall, it *might* slightly shrink the core library (perhaps 5%) but would add to the maintenance costs, as each of those strange new extensibility points would have to be supported forever.

    I can understand why jQuery chose to release 1.9 (with old-IE support) and jQuery 2.0 (for IE9+ only). Practically, it’s a lot more straightforward than trying to do a plugin.

    > IE8 is still used more than IE9… wondering if this can be shimmed like the way ‘bind’ is shimmed in knockout ?

    Indeed – this plugin is only intended for the kind of cases I listed where you target ES5 browsers. There are lots of projects in that category, though sadly not all projects for a while yet :) This is a forward-looking plugin that offers benefits in the ES5 case without stopping KO-core from continuing to support ES3. As for shimming, unfortunately not: I don’t believe there’s any reasonably way of shimming property getters/setters in ES3, hence the ES5 dependency. KO’s existing syntax was carefully chosen to be the best that can be done on ES3 browsers.

  6. Very nice! I had done some research in this area as well a while back and ran into some integration challenges. Just wanted to mention to people that use this plugin to be careful with how it interacts with existing KO plugins, as many are expecting to be dealing with observables/observableArrays specifically at this point. Steve- I had been thinking about a binding provider that would supply bindings with the actual observables to potentially work around this challenge.

  7. Thanks for the reply Steve. I guessed that a legacy plugin would require jumping through hoops, but I I didn’t realise how many versus the potential gain of stripping it. 5% isn’t worth the effort

    Keep up the good work, Sir!

  8. Markus Klug

    This is fantastic! Are there any performance implications at all?

  9. Kim Tranjan

    Awesome! That’s great to old “users”! :)

  10. Erik

    Nice improvement!

    Perhaps I’d better ask Ryan about this, but how will the usage of getters and setters impacts the usage of the Simple Editor Patter as described on http://www.knockmeout.net/2013/01/simple-editor-pattern-knockout-js.html? Ryan uses ‘sub-observable’ to hide data. This will no longer work with a getter in place.
    Are you planning to support this kind of functionality in future versions, or do you recommend a different way for implementing this kind of functionality?

  11. John

    I think Josh Olson’s comment is very valid. Where/how does this fit in with the mapping plugin? On the face of it there seems to be feature overlap. When should one choose to use either of these plugins over the other?

  12. How would you, or can you, use the new computed syntax with the computed’s deferEvaluation, or throttle setting?

  13. “Server-side code running inside Node.js” How is Knockout used on server side? And what’s its use there?

  14. And most the plugin code is comments anyways :) . As always admire your great work. I would still stick with angularJS since it watches what it needs to update in the view (which is generally less) instead of what you have in your model (which can be quite more). Not an issue if you are in control of what you make observable but with automatic tracking its one more step to configure and knockout manages even when that particular property is not used in view.

  15. Why not knockoutjs follow Jquery 2.0/Jqiery Migration pattern?

  16. Dung

    How to use with extend ?
    self.UserName = ko.observable().extend({
    required: {
    message: “The UserName is required”
    },
    maxLength: {
    params: 50,
    message: “The maximum length of UserName is 50″
    }
    });

  17. If you replace computed properties with fields, how do you then track other computed properties that depend upon them? I thought the purpose of the function call to access the value was two-fold: first, recompute if necessary, and second, record the dependency.

    Thanks.

  18. Great work Steven, could you please put a version on that plugin and push to NuGet.

    Thanks

  19. BuddyP

    I know you are a busy guy, but it has been 3 months since you mentioned adding info on how you developed touralot.

    Really need to see some of the details.

    Thanks

  20. Alex.G

    Gene Reddick, I guess you could do it as follow:

    ko.defineProperty(this, ‘subtotal’, function() {
    return this.price * this.quantity;
    });

    ko.getObservable(this, ‘subtotal’).extend({ throttle: 500 });

    Steve, wouldn’t it be better if defineProperty returned the newly created observable instead of the target object to ease this operation?

  21. Paul Valla

    Unless i miss something, knockout-es5 use WeakMap which is part of the Harmony (EcmaScript 6) proposal, it’s currently only available in firefox.

    For chrome you need to go to chrome://flags and and activate the entry “Enable Experimental JavaScript”. If you don’t want the user to activate the entry, you need to use a WeakMap Shim for example: http://benvie.github.io/WeakMap/

  22. Alex.G

    There’s a weakmap shim provided in the same repository.

  23. Alex.G

    Knockout 3.0 + ES5 plugin = Quite elegant! (Scroll the JS bit to the very bottom)

    http://jsfiddle.net/2Epfp/25/

  24. Woah this website is usually spectacular i love reading through your posts. Be the good work! You know, a lot of persons are usually looking game just for this data, you could potentially guide these people considerably.

  25. Hey, Great work Steven, could you please put a version on that plugin and push to NuGet!

  26. What’s up to all, how is the whole thing, I think every one is getting more from this web site, and your views are fastidious designed for new users.

  27. The script insertion seems to rely on a sync load. My loader does everything async and I can’t control if ko will be down by the time ko-es5 is down. Is this the case? I also tried to put them both in the same file put that didn’t work either…

  28. Gene

    I see a node module loader but not a require/and loader. Any reason that isn’t included?

  29. Tyrsius

    Steve, it would be nice to be able to use something like defineProperty to attach a regular observable to an object as a one off, instead of calling ko.track again with a single property.

  30. nice articles. regards from turkey.

  31. Carlos

    Came up with a class to track nested objects using ko.track of knockout.es5.

    http://carlosonlineprogramming.blogspot.com/2013/09/knockout-es5-track-nested-objects.html

    Knockout ES5 track nested objects
    /* Knockout ES5 track nested objects: TypeScript version.
    Solves the problem of ko.track not traversing into nested objects.
    Calls ko.es5.track on nested objects: allowing tracking of strings, numbers, & arrays.
    Use ko.es5.computed(function() …) to mark computed functions. ko.es5.track will call ko.defineProperty on these marked functions, after calling ko.track on the primitive members.
    Excludes nested functions/objects with named constructors. Idea is that TypeScript classes with constructors can call ko.es5.track themselves. This exclusion can be removed.
    Does not traverse into nested classes/objects with named constructors will not be traversed. These constructors should call ko.es5.track themselves.
    */

    interface KnockoutStatic {
    es5: {
    computed: Function;
    track: Function;
    };
    }

    module koES5 {
    function getType(x) {
    if ((x) && (typeof (x) === “object”)) {
    if (x.constructor === Date) return “date”;
    if (x.constructor === Array) return “array”;
    }
    return typeof x;
    }

    export class Track {
    mapped = [];

    constructor(private rootObject: any) {
    this.track(rootObject);
    this.clearAllMapped();
    }

    track(source, name: string = null) {
    if (source == null || this.isMapped(source))
    return;
    if (name == null)
    name = this.name(source);

    var keys = [];
    var computed = [];
    this.setMapped(source);

    for (var key in source) {
    var value = source[key];
    var type = getType(value);

    switch (type) {
    case “array”:
    case “string”:
    case “number”:
    //console.log(name + “.” + key, type);
    keys.push(key);
    break;

    case “function”:
    if (this.isComputed(value)) {
    //console.log(“f> ” + name + “.” + key, type);
    computed.push({
    name: key,
    fn: value
    });
    }
    break;

    case “object”:
    if (value == null || this.isMapped(value) || !this.isTrackable(value) || !this.isTrackableField(key))
    continue;

    //console.log(“o> ” + name + “.” + key, type);
    this.track(value, key);
    break;
    }
    }

    if (keys.length > 0) {
    ko.track(source, keys);
    }

    if (computed.length > 0) {
    computed.forEach((item) => {
    this.makeComputed(source, item.name, item.fn);
    });
    }
    }

    private name(value) {
    var xtor = value.__proto__.constructor;
    return xtor !== undefined && xtor.name != undefined ? xtor.name : “”;
    }

    private isTrackable(value) {
    var xtor = value.__proto__.constructor;
    return xtor.name === “Object”;
    }

    private isTrackableField(key: string) {
    return key != “__ko_mapping__”;
    }

    private isMapped(value: any) {
    return (value.__tracked__ === true);
    }

    private setMapped(value: any) {
    if (this.isMapped(value))
    return;
    value.__tracked__ = true;
    this.mapped.push(value);
    }

    private clearAllMapped() {
    this.mapped.forEach((value) => {
    delete value["__tracked__"];
    });
    this.mapped.unshift();
    }

    private isComputed(fn: Function) {
    return (fn["__ko_es5_computed__"] === true);
    }

    private makeComputed(container: any, name: string, fn: Function) {
    var nameOverride = fn["__ko_es5_computed_name__"];
    if (nameOverride !== undefined && nameOverride !== “”) {
    name = nameOverride;
    delete fn["__ko_es5_computed_name__"];
    }

    if (name === undefined || name == “”) {
    console.log(“Error. Function missing name”, fn);
    return;
    }
    ko.defineProperty(container, name, fn);
    delete fn["__ko_es5_computed__"];
    }
    }

    export function track(root: any) {
    new Track(root);
    }

    export function computed(fn: Function, name: string = null) {
    fn["__ko_es5_computed__"] = true;
    if (name || false) {
    fn["__ko_es5_computed_name__"] = true;
    }
    return fn;
    }

    ko.es5 = {
    computed: koES5.computed,
    track: koES5.track,
    };
    }

  32. Kiril Kostov

    I think this should become a native part of knockout framework. The older crap browsers should not be tolerated. It’s a big pain in the ass to support them just because someone not install one of the standard friendly browsers.

  33. Hello there, I found your website via Google while looking for a comparable topic, your website came up,
    it seems great. I have bookmarked it in my google bookmarks.

    Hello there, just became aware of your blog thru Google, and found that it is really informative.
    I am going to be careful for brussels. I will appreciate if you
    happen to proceed this in future. Numerous other people can be benefited from
    your writing. Cheers!

  34. Greetings! Very useful advice within this post!
    It is the little changes that produce the greatest changes.
    Many thanks for sharing!

    Review my website :: ダウンジャケット 品質合格

  35. Greetings! Very useful advice in this particular article!
    It’s the little changes that will make the most important changes.
    Many thanks for sharing!

  36. I’m really impressed with your writing skills
    as well as with the layout on your blog. Is this
    a paid theme or did you customize it yourself?
    Either way keep up the excellent quality writing, it is rare to see a nice blog like this one today.

    Here is my webpage 推薦 ランニングシューズ

  37. Thank you for the auspicious writeup. It in fact was a amusement account it.
    Look advanced to more added agreeable from you!
    By the way, how could we communicate?

    Also visit my blog post: 古着 正規

  38. I really love your website.. Very nice colors & theme.
    Did you make this site yourself? Please reply back as I’m trying to create my very own blog and want to learn where
    you got this from or exactly what the theme is called. Many thanks!

    Also visit my web blog; 2013 ナイキ シューズ

  39. Hey there, You have done a great job. I’ll certainly digg
    it and personally recommend to my friends. I am confident they will be benefited from this web site.

    Here is my web blog … webpage (Cathy)