Knockout.js Observable Extensions

On December 27, 2011, in samples, by Josh Bush

This started out as a post about how to implement the new extender feature in Knockout.js 2.0. I wanted to see how well that would improve the experience of a money observable I created several months back. Once I had it implemented though, I was a bit disappointed. My extender doesn’t have any arguments, but the knockout observable extend call only accepts a hash in the form of {extenderName:extenderOptions}. I ended up with a call that looked like this: var cash=ko.observable(5.23).extend({money:null});

That didn’t leave a very good taste in my mouth. So, I pulled down knockout and set out to change the way the extenders were implemented. I’ve grown fond of how jQuery chaining worked, so why not bring that to Knockout’s observables? Luckily Ryan Niemeyer was there to save me from myself and pointed out that I could just extend ko.subscribable.fn to achieve the desired effect.

I’m happy with the outcome. Let’s explore the strategy a bit. Before I get in too deep, here’s the end result:


Click here for full jsFiddle

You may be asking yourself, “What’s so great about this?” This is basically the same as my previous sample with one exception. This implementation attaches directly to the subscribable type that KO provides. You might not have seen this unless you’ve spent some time digging around the knockout.js source. This type serves as a base for observables, obervableArrays and dependentObservables computed observables.

Here’s the code that provides the money formatting:

(function(){
    var format = function(value) {
        toks = value.toFixed(2).replace('-', '').split('.');
        var display = '$' + $.map(toks[0].split('').reverse(), function(elm, i) {
            return [(i % 3 === 0 && i > 0 ? ',' : ''), elm];
        }).reverse().join('') + '.' + toks[1];

        return value < 0 ? '(' + display + ')' : display;
    };

    ko.subscribable.fn.money = function() {
        var target = this;

        var writeTarget = function(value) {
            target(parseFloat(value.replace(/[^0-9.-]/g, '')));
        };

        var result = ko.computed({
            read: function() {
                return target();
            },
            write: writeTarget
        });

        result.formatted = ko.computed({
            read: function() {
                return format(target());
            },
            write: writeTarget
        });

        return result;
    };
})();

Breakdown
Line 11 is where we start. By extending the subscribable.fn object we are adding a property to each and every subscriabable object that KO creates for us. This will give us the ability to chain observables to one another as long as we return an observable from our method(line 32).

On line 12 we see that 'this' references the observable we're extending. I like this because there are no special method signatures we need to implement. Here I'm just grabbing my own reference of this as a variable named target.

Line 18 is where this starts to get a little interesting. I'm creating a writable computed observable that will return the value from the base observable when read. When it gets written to, it will sanitize the input and then write that to the base observable. This will be the observable we return for public consumption(line 32).

Line 25 is where the formatting comes into play. To the observable we're returning we'll add another observable as a property named 'formatted'. This is what we'll bind to whenever we want to see a pretty version of our value. This is another read/write computed observable like we did above. When the property is read from, it will pass the base observable's value through a formatter. The write is the same as the base observable.

Use It

var viewModel = {
    Cash: ko.observable(-1234.56).money(),
    Check: ko.observable(2000).money(),
    showJSON: function() {
        alert(ko.toJSON(viewModel));
    }
};

viewModel.Total = ko.computed(function() {
    return this.Cash() + this.Check();
}, viewModel).money();
ko.applyBindings(viewModel);

On lines 2,3, and 11 you can see where I've used the observable extension I created above. The cool thing about this technique is that we don't care what kind of observable we're extending, it just works.

The showJSON function on line 4 is what gets fired when we click the "Show View Model JSON" button on the example above. Click this and you will see that our json serialization is clean. This is because the base observable we return is the unformatted (no dollar signs, commas, or parenthesis) version.

The Payoff

<div class='ui-widget-content'>
    <p>
        <label>How much in Cash?</label>
        <input data-bind="value:Cash.formatted,css:{negative:Cash()<0}" />
    </p>
    <p>
        <label>How much in Checks?</label>
        <input data-bind="value:Check.formatted,css:{negative:Check()<0}" />
    </p>
    <p>
        <label>Total:</label>
        <span data-bind="text:Total.formatted,css:{negative:Total()<0}" />
    </p>
    <p>
        <button data-bind="click:showJSON">Show View Model JSON</button>
    </p>
</div>

Lines 4 and 8 we've bound the input's value to the formatted version of the extended observable. Line 12 has the text of a span bound to the formatted version of the computed observable.

I've rehashed this example 3 times now, but I'm happiest with this implementation. Extending *.fn.* isn't documented anywhere I saw, but maybe it should be. ;) Maybe I should RTFM, it's clearly documented here. This chaining technique will be familiar to anyone who has used jQuery. What do you think about this technique?

Tagged with:  
  • Steve

    Looks good to me! This is exactly what “fn” is intended for.

    BTW, it is documented, here: http://knockoutjs.com/documentation/fn.html

    Steve

    • Anonymous

      Thanks, I updated the post with a link to the documentation.

    • Anonymous

      Steve, if you have time: I’d love to know where this fits in with the new extender bits. I can’t see any reason to use it over this technique.

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1010

  • Erik_F

    This is great stuff – I’m just getting into Knockout, considering using it for a personal project – and examples like this are exactly what I was looking for in how to extend the library. Thanks a bunch =)

    • Anonymous

      One thing that’s impressed me with Knockout.js has been how well it fits in to my workflow. It’s not overly prescriptive about how you have to write your code. 

  • http://knockmeout.net Ryan Niemeyer

    I like it!  No great advantage to using extenders over this technique.  They do help keep the API surface area low on the subscribables (code is really not needed outside of creation) and provide a pattern/structure that people can follow for customizing how their objects are initialized.  

    • Anonymous

      Thanks for the feedback. I’ve got a few more ideas to try out now that I’ve discovered the KO extension story.

  • Andreas

    Brilliant stuff!