Using jQuery UI progress bar with MVVM, Knockout and web workers

One of the popular questions asked by people who start using Knockout is 'how do I integrate it with third party widgets?'. That is perfectly understandable, as there is an enormous amount of existing JavaScript UI widgets that can make our (i.e. developers') lives much easier.

In this post I would like to explore:

  • how to use jQuery UI progress bar with Knockout and MVVM pattern, as a simple example of reusing existing JavaScript UI components,
  • how to use web workers to execute long running task asynchronously and notify view model about the results and progress

Knockout by itself does not provide UI components as it serves and fits a different purpose. Out of the box it provides a set of built-in bindings that integrate with standard HTML elements (such as button, select, input, and others). To include support for 3rd party UI components one can create custom bindings that provide code specific to given element.

Let's say we want to create a page that includes some time consuming process (usually communication with external services or maybe some kind of number crunching) that can be measured (so that progress can be reported to the user). For the sake of example I will be calculating N first prime numbers.

The view model can look like that:

koprogressbar.viewModel = (function () {  
    //prime number count limit
    var limit = ko.observable(0);

    // determines whether the task is active
    var isBusy = ko.observable(false);

    // array of primes found
    var primesFound = ko.observableArray();

    // prime number count, in case you want to bind to it
    var numberCount = ko.computed(function () {
        return primesFound().length;
    });

    //define progress as a simple computed observable
    var progress = ko.computed(function () {
        return numberCount() / limit() * 100;
    }, this);

    function startTask() {
        //...
    }

    return {
        progress: progress,
        isBusy: isBusy,
        primesFound: primesFound,
        startTask: startTask
    };
})();

If you wonder, koprogressbar is a namespace defined as follows

// create namespace (or reuse existing one)  
var koprogressbar = koprogressbar || {};

I am also using revealing module pattern to create the view model (hence the function and vars inside of it). If you find this confusing and over-complicated just use object literal (or any other technique) :)
The view model is simple and contains hopefully self-explanatory properties.
Once we have this code ready we can continue by adding support for jquery UI progress bar in the form of custom binding.

koprogressbar.createCustomBindings = function () {  
    ko.bindingHandlers.progressBind = {
        init: function(element, valueAccessor) {
            $(element).progressbar({
                value: valueAccessor()()
            });
        },
        update: function (element, valueAccessor) {
            //assign observable value to progress bar value
            $(element).progressbar("value", valueAccessor()());
        }
    };
};

Custom binding provides two callbacks init and update. Init is executed by knockout once for each element you use the binding on. In this case we want to take the element (which will be a <div>) and call progressbar() function on it that initializes it as a jQuery progress bar.

The other callback - update will be executed every time our observable (progress in this case) changes. Here we simply update the value of the progress bar.

The code for this widget is really simple as we don't need to provide two-way binding (that is from the widget to observable).

Here is a crude HTML page I used for this example:

<html>  
<head>
    <title>title</title>
    <script type="text/javascript" src="/Scripts/jquery-1.4.4.js"></script>
    <script type="text/javascript" src="/Scripts/jquery-ui-1.8.20.js"></script>
    <script type="text/javascript" src="/Scripts/knockout-2.1.0.js"></script>
    <script type="text/javascript" src="/Scripts/Page.js"></script>
    <link rel="stylesheet" href="/Content/themes/base/jquery.ui.theme.css"/>
    <link rel="stylesheet" href="/Content/themes/base/jquery.ui.progressbar.css"/>
</head>
<body>
    <div style="width:40%;margin:auto;">
        <div id="progressBar" data-bind="progressBind: progress"> </div>
        <div style="width: 100%; text-align: center;">
            <button 
                style="width:75px; height: 30px; margin: 0 auto; left:50px; margin-top: 15px;" 
                data-bind="disable: isBusy, click:startTask">
                Calculate
            </button>
        </div>
    </div>
</body>
</html>

Every time the progress property of the view model changes, the progress bar will be updated automatically. You can test it by using javascript console - koprogressbar.viewmodel.progress(some number);

Now, let's implement the number crunching process. We will use web workers for that purpose which provide asynchronous execution in javascript. Please note that IE9 does not support web workers at this stage (iirc IE10 will).

The main concept behind web workers is simple. You provide javascript file that contains the code to be executed by a worker. Once you create worker the code gets executed asynchronously (probably in a separate thread). You can send information in and out of the worker using messages.

Here is the missing startTask() implementation from the view model that spins off web worker (code defined in Task.js).

function startTask() {  
        isBusy(true);
        if (worker != undefined) {
            worker.terminate();
        }
        //clear results (if any)
        primesFound.removeAll();
        limit(primeNumberLimit);
        worker = new Worker('Scripts/Task.js');
        worker.onmessage = workerOnMessage;
        //start the worker
        worker.postMessage(null);
    }

Note that we specify onmessage callback that will be used to pass data from worker to our view model.

function workerOnMessage(e) {
        console.log('Found prime number: ' + e.data.toString()
            + ' total found: ' + (numberCount() + 1).toString());
        primesFound.push(e.data);
        if (numberCount() >= limit()) {
            worker.terminate();
            isBusy(false);
            console.log('Finished!');
        }
        console.log('Progress: ' + progress());
    }

We need to execute initialization code in our page as well:

$(function () {  
    koprogressbar.createCustomBindings();
    ko.applyBindings(koprogressbar.viewModel);
});

Finally, here is Task.js worker code. Im using setTimeout() function to introduce an artificial delay to simulate a long running process.

var number = 2;

onmessage = function() {  
    findPrime();
}

function findPrime() {  
    var isPrimeNum = isPrime(number);
    if (isPrimeNum) {
        postMessage(number);
    }
    number++;
    setTimeout("findPrime()", 100);
}

function isPrime(num) {  
    for (var i = 2; i <= Math.sqrt(num) ; i += 1)
        if (num % i == 0) {
            // not a prime
            return false;
        }
    return true;
}

Completed example will behave like this: