Sunday, February 20, 2011


While working on a recent HTML5 project I found myself wishing for the convenience of data binding.  I was building a timer and was adding a lot of callbacks to update the UI elements on the page.  I figured there was a better way and after a little searching I came across Knockout.

Our Goal



Knockout is a great combination of jQuery templates and data binding that allows for one-way and two-way binding back to a Javascript object.  I've put together an example project with an ugly looking stopwatch, and we'll go through some parts of it in this blog post (just want the code, bro? get it here)

The timer() class with Knockout Observables

/// The timer class.
var timer = function () {
    this.started = new ko.observable(false);
    this.totalSeconds = new ko.observable(0);

    this.seconds = new ko.dependentObservable(function () {
        return (this.totalSeconds() % 60).toFixed(0);
    }, this);

    this.secondsDisplay = new ko.dependentObservable(function () {
        var secs = this.seconds();
        var display = '' + secs;
        if (secs < 10) { display = '0' + secs; }

        // Hack for weird edge case because of setInterval.
        if (display == '010') { display = '10'; }

        return display;
    }, this);

    this.minutes = new ko.dependentObservable(function () {
        return ((this.totalSeconds() / 60) % 60).toFixed(0);
    }, this);

    this.hours = new ko.dependentObservable(function () {
        return (((this.totalSeconds() / 60) / 60) % 60).toFixed(0);
    }, this);

    this.secondHandAngle = new ko.dependentObservable(function () {
        return this.seconds() * 6;
    }, this);

    this.minuteHandAngle = new ko.dependentObservable(function () {
        return this.minutes() * 6;
    }, this);

    this.hourHandAngle = new ko.dependentObservable(function () {
        return this.hours() * 6;
    }, this);

    this.alarm = function () {
        log('alarm fired');
    };
};

// timer.start
timer.prototype.start = function () {
    this.started(true);
    this.startTime = new Date();
    var self = this;

    this.intervalId = setInterval(function () {
        var oldTime = self.startTime;
        self.startTime = new Date();

        var diff = secondsBetween(self.startTime, oldTime);
        var currSeconds = self.totalSeconds();
        self.totalSeconds(currSeconds + diff);
    }, 100);
};

// timer.stop
timer.prototype.stop = function () {
    this.started(false);
    if (this.intervalId) {
        clearInterval(this.intervalId);
    }
};

// helper...
function secondsBetween(date1, date2) {
    return (date1.getTime() - date2.getTime()) / 1000;
};

From the timer class code you can see that we declare our fields as ko.observable()'s.  These are wrappers around our values that help with notifying our bound elements when the values change.  The function() syntax for accessing the fields does take a little getting used to; evidently it's necessary since IE doesn't implement property setters and getters.  Other than the new syntax, our class is pretty basic.  We have a started field and a totalSeconds field that drive the rest of our fields by way of the dependentObservable() functionality.  The dependentObservable() is a nifty way of declaring fields that are computed based on other observable() fields.  For us, we do some quick math to determine our seconds, minutes and hours based on the totalSeconds that have passed while running the timer.  We also create some fields for the angle of our timer hands based on the computed underlying second/minute/hour values.


The ViewModel and View DataBinding

@{
 Page.Title = "Home Page";
}

@section ScriptSection {

    <script id="faceTemplate" type="text/x-jquery-tmpl">
        @* Our SVG Code in a partial view *@
        @Html.Partial("_WatchFace")
        <div id="timerInfo" style="font-weight: bold; font-size: 24px; float: left;">
            <span class="minutes" data-bind="text: minutes()"></span>
            <span>:</span>
            <span class="seconds" data-bind="text: secondsDisplay()"></span>
        </div>
    </script>

    <script type="text/javascript">

        // Our custom svg Rotate transform binding...
        ko.bindingHandlers.svgRotate = {
            init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
                // This will be called when the binding is first applied to an element
                // Set up any initial state, event handlers, etc. here
            },
            update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
                // This will be called once when the binding is first applied to an element,
                // and again whenever the associated observable changes value.
                // Update the DOM element based on the supplied values here.

                var value = valueAccessor(), allBindings = allBindingsAccessor();

                var rotation = ko.utils.unwrapObservable(value);
                var originX = allBindings.originX || 0;
                var originY = allBindings.originY || 0;

                var rotateText = 'rotate(' + rotation + ', ' + originX + ', ' + originY + ')';
                var id = $(element).attr('id');

                // Using the old school doc getElement because jquery's attr() is not setting the value correctly
                var elem = document.getElementById(id);
                if (!elem) { log('rotate binding element not found'); return; }

                elem.setAttribute('transform', rotateText);
            }
        };

        // Our page ViewModel
        var viewModel = {
            watch: new ko.observable(new timer()),
            start: function () {
                this.watch().start();
            },
            stop: function () {
                this.watch().stop();
            },
            toggleTimer: function () {
                this.watch().started() ? this.stop() : this.start();
            }
        };

        ko.applyBindings(viewModel);
    </script>
}

<p>
    This is an example project using <a href="http://html5boilerplate.com">HTML5 Boilerplate</a> patterns, <a href="http://knockoutjs.com">Knockout.js</a>  MVVM Binding and Templating, <a href="http://docs.jquery.com/Qunit">QUnit</a> unit tests and SVG Graphics.
</p><br />

<div id="watchContainer" style="cursor: pointer;" data-bind='template: { name: "faceTemplate", data: watch()}, click: toggleTimer'>

</div>

The main parts of this code are the ViewModel which we bind our HTML Elements to, and the jQuery templates that define our stopwatch and second/minute hands.  At the top, I declare my SVG Stopwatch with a custom knockout binding (you can see the code for the special binding at lines 10-20) I created to update the transform of the path based on the angle in the timer.

Next, the script section for our page creates a special binding that updates the transform attribute of the Path SVG Element to rotate the hands of the clock.  Also in the script section, we declare our ViewModel for the page.  The ViewModel creates a timer and some utility functions for toggling the timer between start and stop.  After declaring our ViewModel, we use Knockout's ko.applyBindings() function to setup the page's templates and apply our data bindings.


The MotherEffin Clock Demo Project

I've put together a Demo Project for download using my MotherEffin HTML5 Boilerplate project template for MVC3.  I highly recommend visiting the KnockoutJS documentation for more information about templatescustom bindings and observables.

Next time, we'll go through Unit Testing and Object Oriented Javascript with QUnit.


Now Playing - LL Cool J - Momma Said Knock You Out

No comments:

Post a Comment