Home > jQuery, MVVM > Creating a custom Knockout binding for the jQuery Mobile ListView

Creating a custom Knockout binding for the jQuery Mobile ListView

Introduction

A shift towards more client side web development is happening in the IT industry. This allows developers to build quicker responding and more user friendly GUIs for their end users. This shift is made possible with the arrival of HTML5 and more powerful JavaScript frameworks. Features like camera access, local storage, drag & drop … previously only available in native applications are now also possible in web applications.

In the past I have already written about jQuery Mobile. It is an excellent JavaScript framework to develop mobile web applications or hybrid applications. Another useful framework for the development of modern web applications is Knockout. This framework makes the MVVM design pattern possible in web applications. MVVM stands for Model-View-ViewModel. It is a software pattern of Microsoft commonly used in WPF and Silverlight applications.

In this article I will explain how to build a custom Knockout binding for the jQuery Mobile ListView.

Context

Why do we need a custom binding for the jQuery Mobile ListView? With what is provided out of the box, it is already possible to bind to a ListView. Unfortunately, we are then rather limited in functionality. Imagine the following example: I have a JavaScript array of food. The food can be divided in a number of categories (fruit, vegetables and snacks). I want to show the food in a jQuery Mobile ListView and show a divider per category.

foodapp

With the foreach binding that is provided out of the box in Knockout, I would have to divide my food in 3 arrays. If not, it would not be possible to add a divider per category.

function FoodViewModel(name, category, image) {
    var self = this;

    self.name = name;
    self.category = category;
    self.image = image;
};

function MainViewModel() {
    var self = this;

    self.fruit = ko.observableArray([
        new FoodViewModel("Apple", "Fruit", "/Images/apple.jpg"),
        new FoodViewModel("Banana", "Fruit", "/Images/banana.jpg"),
        new FoodViewModel("Pear", "Fruit", "/Images/pear.jpg")
    ]);

    self.vegetables = ko.observableArray([
        new FoodViewModel("Carrot", "Vegetables", "/Images/carrot.jpg"),
        new FoodViewModel("Tomato", "Vegetables", "/Images/tomato.jpg"),
    ]);

    self.snacks = ko.observableArray([
        new FoodViewModel("Cookie", "Snacks", "/Images/cookie.jpg")
    ]);
};
<ul data-role="listview" data-divider-theme="b">
    <li data-role="list-divider">Vegetables</li>
    <!-- ko foreach: vegetables -->
    <li>
        <a href="#">
            <img data-bind="attr: { src: image }" />
            <h3 data-bind="text: name"></h3>
        </a>
    </li>
    <!-- /ko -->

    <li data-role="list-divider">Fruit</li>
    <!-- ko foreach: fruit -->
    <li>
        <a href="#">
            <img data-bind="attr: { src: image }" />
            <h3 data-bind="text: name"></h3>
        </a>
    </li>
    <!-- /ko -->

    <li data-role="list-divider">Snacks</li>
    <!-- ko foreach: snacks -->
    <li>
        <a href="#">
            <img data-bind="attr: { src: image }" />
            <h3 data-bind="text: name"></h3>
        </a>
    </li>
    <!-- /ko -->
</ul>

I found this quite limiting, because I preferred to have one list containing all my food.

The jqmListView custom binding

In order to resolve this issue, I decided to develop a custom binding for the jQuery Mobile ListView. I based myself on the source of the foreach binding and customized it to have support for the features of the ListView.

It can be applied in two ways:

  1. <ul data-role=”listview” data-divider-theme=”b” data-bind=”jqmListView: food”>
  2. <ul data-role=”listview” data-divider-theme=”b” data-bind=”jqmListView: { data: food, divider: generateDivider, dividerCompareFunction: sortFood, itemCompareFunction: sortItems }”>

The properties “divider”, “dividerCompareFunction” and “itemCompareFunction” are optional.

divider

The property “divider” allows to customize the generation of the dividers for the ListView. It can be assigned a function that will called for every item in the list bound to the ListView. The function must return the category name for each item.

An example:

self.generateDivider = function (data) {
	return data.category;
};

dividerCompareFunction

The property “dividerCompareFunction” allows to customize the sorting of the dividers. By default they are sorted alphabetically.

An example:

self.sortFood = function (divider1, divider2) {
	var weights = new Object;
	weights["Vegetables"] = 1;
	weights["Fruit"] = 2;
	weights["Snacks"] = 3;

	return weights[divider1] - weights[divider2];
};

itemCompareFunction

The property “itemCompareFunction” allows to customize the sorting of the items of a given category. By default they are not sorted in order of appearance in the JavaScript list.

An example:

self.sortItems = function (item1, item2) {
	return item1.name.localeCompare(item2.name);
}

Sample Code

If we use the jqmListView binding, we can rewrite our sample application:

function FoodViewModel(name, category, image) {
    var self = this;

    self.name = name;
    self.category = category;
    self.image = image;
};

function MainViewModel() {
    var self = this;

    self.food = ko.observableArray([
        new FoodViewModel("Carrot", "Vegetables", "/Images/carrot.jpg"),
        new FoodViewModel("Apple", "Fruit", "/Images/apple.jpg"),
        new FoodViewModel("Pear", "Fruit", "/Images/pear.jpg"),
        new FoodViewModel("Tomato", "Vegetables", "/Images/tomato.jpg"),
        new FoodViewModel("Banana", "Fruit", "/Images/banana.jpg"),
        new FoodViewModel("Cookie", "Snacks", "/Images/cookie.jpg")
    ]);

    self.generateDivider = function (data) {
        return data.category;
    };

    self.sortFood = function (divider1, divider2) {
        var weights = new Object;
        weights["Vegetables"] = 1;
        weights["Fruit"] = 2;
        weights["Snacks"] = 3;

        return weights[divider1] - weights[divider2];
    };

    self.sortItems = function (item1, item2) {
        return item1.name.localeCompare(item2.name);
    }
};
<ul data-role="listview" data-divider-theme="b" data-bind="jqmListView: { data: food, divider: generateDivider, dividerCompareFunction: sortFood, itemCompareFunction: sortItems }">
    <li>
        <a href="#">
            <img data-bind="attr: { src: image }" />
            <h3 data-bind="text: name"></h3>
        </a>
    </li>
</ul>

The food sample application is a very simple application were the differences between the foreach and the jqmListView bindings are minor. But the jqmListView is a lot more flexible then the foreach binding in more complex cases. For example: if the data is coming from the server using a REST call and we don’t know beforehand all the available categories that will send to the browser.

You can download the sample from: http://sdrv.ms/Ol0LRY

jqmListView Code

Below you can find the source code  of my jqmListView binding. I will also upload this to GitHub.

(function () {
    ko.bindingHandlers.jqmListView = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            // Support anonymous templates
            var bindingValue = ko.utils.unwrapObservable(convertToBindingValue(valueAccessor));

            if ((element.nodeType == 1 || element.nodeType == 8)) {
                // It's an anonymous template - store the element contents, then clear the element
                var templateNodes = element.nodeType == 1 ? element.childNodes : ko.virtualElements.childNodes(element),
                    container = ko.utils.moveCleanedNodesToContainerElement(templateNodes); // This also removes the nodes from their current parent
                new ko.templateSources.anonymousTemplate(element)['nodes'](container);
            }
            return { 'controlsDescendantBindings': true };
        },
        update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            var bindingValue = ko.utils.unwrapObservable(convertToBindingValue(valueAccessor));

            // Clean the current children
            $(element).empty();

            var dataArray = (bindingValue['data']) || [];
            var dividerFor = bindingValue['divider'];

            var dividerDictionary = new Object();
            for (var i = 0; i < dataArray.length; i++) {
                var dividierName = dividerFor(dataArray[i]);

                dividerDictionary[dividierName] = (dividerDictionary[dividierName]) || [];
                dividerDictionary[dividierName].push(dataArray[i]);
            }

            $.each(sortKeys(dividerDictionary, bindingValue.dividerCompareFunction), function (index, key) {
                if (key !== "") {
                    $(element).append('<li data-role="list-divider">' + key + '</li>');
                }

                var tempElement = document.createElement("div"); // Create temp DOM element to render templating
                ko.renderTemplateForEach(element, dividerDictionary[key].sort(bindingValue.itemCompareFunction), /* options: */bindingValue, tempElement, bindingContext);
                $(element).append($(tempElement).children()); // Add data to listview
            });

            $(element).listview('refresh');
        }
    };

    function convertToBindingValue(valueAccessor) {
        /// <summary>Standardizes the properties of the of the valueAccessor object.</summary>
        /// <returns>An object containing standardized binding properties.</returns>

        var bindingValue = ko.utils.unwrapObservable(valueAccessor());

        // If bindingValue is the array, just pass it on its own
        if ((!bindingValue) || typeof bindingValue.length == 'number')
            return {
                'data': bindingValue,
                'divider': function () { return ""; },
                'templateEngine': ko.nativeTemplateEngine.instance
            };

        // If bindingValue.data is the array, preserve all relevant options
        return {
            'data': ko.utils.unwrapObservable(bindingValue['data']),
            'divider': bindingValue['divider'] || function () { return ''; },
            'dividerCompareFunction': bindingValue['dividerCompareFunction'],
            'itemCompareFunction': bindingValue['itemCompareFunction'],
            'includeDestroyed': bindingValue['includeDestroyed'],
            'afterAdd': bindingValue['afterAdd'],
            'beforeRemove': bindingValue['beforeRemove'],
            'afterRender': bindingValue['afterRender'],
            'templateEngine': ko.nativeTemplateEngine.instance
        };
    };

    function sortKeys(data, compareFunction) {
        /// <summary>Convert the properties of a given object to an array and return them in sorted order.</summary>
        /// <param name="data">The object who's properties must be sorted.</param>
        /// <param name="compareFunction">The compare function that must be used during sorting.</param>
        /// <returns type="Array">The sorted properties of the object.</returns>

        var keys = Array();

        for (var key in data) {
            keys.push(key);
        }

        return keys.sort(compareFunction);
    }
})();
About these ads
Categories: jQuery, MVVM
  1. Kev
    December 20, 2012 at 20:27 | #1

    Nice job. Very helpful

  2. Mahesh
    January 25, 2013 at 22:34 | #2

    Thanks, been looking for a solution like this for days.

    One small change I made is instead of $(element).listview(‘refresh’), I’d put it around a try catch statement.

    try
    {
    $(element).listview(‘refresh’);
    }
    catch (e) {

    }

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: