Home > English > Knockout and Custom Binding Providers

Knockout and Custom Binding Providers

Since I have such a strong background with XAML technologies, Knockout.js has been a natural fit for my development style. I really like the ability to program in a MVVM approach. Recently, I watched Ryan Neimeyer’s session he gave at the That Conference and I was really impressed with being able to create custom binding providers for Knockout.

When developing applications in XAML, I have always loved using Caliburn.Micro as I found Rob Eisenberg’s implementation of convention over configuration to be a great time saver and value add when trying to build applications quickly. Well, with that said, I wanted to create a simple version that would facilitate using conventions but also allow you to provide your own data-bind statements when you wanted to as well.

Before we jump over to my sample on jsFiddle, I wanted to walk you through the simple rules that I have put in place. First of all, I wanted my code to be a minimalistic as possible.

I tried using the name attribute of an element but it turns out that not all DOM children expose the name attribute. With that, I decided to go with use the data-name attribute that is supported with HTML5.

The following link is a sample test page on jsFiddle that demonstrates my solution.

I will walk you through the code required to build my custom binding provider for Knockout.js.

//knockout-conventionBindingProvider v0.0.11 | (c) 2012 Matt Duffield | http://www.opensource.org/licenses/mit-license
!(function (factory) {
    //AMD
    if (typeof define === "function" && define.amd) {
        define(["knockout", "exports"], factory);
        //normal script tag
    } else {
        factory(ko);
    }
}(function(ko, exports, undefined) {
    //a bindingProvider that uses something different than data-bind attributes
    //  options - is an object that can include "attribute" and "valueUpdate" options
    //    "attribute"       - this is the name of the attribute we are trying to check for binding
    //    "valueUpdate"     - this is the binding event that is fired, e.g. change, keyup, keypress, afterkeydown
    //    "anchorTypes"     - represents binding to an A tag
    //    "buttonTypes"     - represents binding to button, submit tags
    //    "checkedTypes"    - represents binding to checkbox and radio tags
    //    "imageTypes"      - represents binding to an IMG tag
    //    "textTypes"       - represents binding to EM, H1, H2, H3, H4, H5, H6, SPAN, STRONG tags
    //    "valueTypes"      - represents binding to password, text, textarea tags
    var conventionBindingProvider = function (options) {
        this.existingProvider = new ko.bindingProvider();

        options = options || {};

        // attribute used for our binding convention.  Default:  "data-name"
        this.attribute = options.attribute || "data-name";

        // defines which browser event KO will use to detect changes. Default: 'change'
        this.valueUpdate = options.valueUpdate || 'change';

        this.anchorTypes = options.buttonTypes || ['A'];
        this.buttonTypes = options.buttonTypes || ['button', 'submit'];
        this.checkedTypes = options.checkedTypes || ['checkbox', 'radio'];
        this.imageTypes = options.imageTypes || ['IMG'];
        this.textTypes = options.textTypes || ['EM', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SPAN', 'STRONG'];
        this.valueTypes = options.valueTypes || ['password', 'text', 'textarea'];

        //determine if an element has any bindings
        this.nodeHasBindings = function (node) {
            if (node.getAttribute && node.getAttribute("data-bind")) {
                return this.existingProvider.nodeHasBindings(node);
            }
            return node.getAttribute ? node.getAttribute(this.attribute) || node.getAttribute("data-focus") : false;
        };

        //return the bindings given a node and the bindingContext
        this.getBindings = function getBindings(node, bindingContext) {
            var result = {};

            // If a binding is already setup, use it.
            if (node.getAttribute && node.getAttribute("data-bind")) {
                result = this.existingProvider.getBindings(node, bindingContext);
            } else {
                var item = node.getAttribute(this.attribute);
                if (item) {
                    //var desc = node.id + " (" + node.nodeName + " - " + node.type + ")";
                    var bindingAccessor = this.mapBindings(node, item, bindingContext);
                    if (bindingAccessor) {
                        var binding = typeof bindingAccessor == "function" ?
                        bindingAccessor.call(bindingContext.$data) : bindingAccessor;
                        ko.utils.extend(result, binding);
                    }
                }
            }

            // Apply focus() on the node containing the data-focus attribute.
            item = node.getAttribute("data-focus");
            if (item != null) {
                node.focus();
            }

            return result;
        };

        this.mapBindings = function (node, bindingName, bindingContext) {
            var obj = {};
            //valueUpdate: 'change'
            obj["valueUpdate"] = this.valueUpdate;

            if (this.isBinding(node.nodeName, this.anchorTypes)) {
                return function () {
                    //click: run
                    obj["click"] = bindingContext.$data[bindingName];
                    return obj;
                };
            }
            else if (this.isBinding(node.type, this.buttonTypes)) {
                return function () {
                    var bindingCanName = "can" + bindingName;
                    //click: run
                    //enable: canrun
                    obj["click"] = bindingContext.$data[bindingName];
                    if (bindingContext.$data[bindingCanName] != null) {
                        obj["enable"] = bindingContext.$data[bindingCanName];
                    }
                    return obj;
                };
            }
            else if (this.isBinding(node.type, this.checkedTypes)) {
                return function () {
                    //checked: isActive
                    obj["checked"] = bindingContext.$data[bindingName];
                    return obj;
                };
            }
            else if (this.isBinding(node.type, this.valueTypes)) {
                return function () {
                    //value: firstName
                    obj["value"] = bindingContext.$data[bindingName];
                    return obj;
                };
            }
            else if (this.isBinding(node.nodeName, this.textTypes)) {
                return function () {
                    //text: title
                    obj["text"] = bindingContext.$data[bindingName];
                    return obj;
                };
            }
            else if (this.isBinding(node.nodeName, this.imageTypes)) {
                return function () {
                    //attr: { src: imagePath };
                    obj["attr"] = {};
                    obj["attr"]["src"] = bindingContext.$data[bindingName];
                    return obj;
                };
            }

            return null;
        };

        this.isBinding = function (value, types) {
            if ($.inArray(value, types) > -1) {
                return true;
            }
            else {
                return false;
            }
        };

    };

    if (!exports) {
        ko.conventionBindingProvider = conventionBindingProvider;
    }

    return conventionBindingProvider;
}));

The first thing you might notice is that I am using Asynchronous Module Definition (AMD) with Require.js in my definition of my custom binidng provider. In the definition of my conventionBindingProvider, you will see that it has only one parameter. I am using a single options parameter that allows the user to define the options behavior for this provider.  Although the default behavior for these options should handle most of the scenarios you want, I like to have the ability to provide my own changes by just passing in an options parameter.

The attribute parameter defaults to data-name but you change this to any attribute you choose. Please note that not all attributes are the same. I wrote above that I originally tried to use the name attribute and ran into issues that not all elements have this attribute. As with Knockout’s default binding using data-bind, I chose to use data-name for my convention.

Next, you have the valueUpdate parameter that defaults to change event. You can use keyup to allow for changes to be triggered when the key is released.

The other option parameters are used for defining the elements for which this binding provider supports. You don’t need to override this but it does give you the flexibility to extend the default behavior without actually modifying the JavaScript file.

When creating a custom binding provider for Knockout, you need to implement two functions: nodeHasBindings and getBindings.

The nodeHasBindings function is used to determine if the corresponding node has the custom binding we with to use. Since we want to support the standard data-bind attribute we return existing binding provider which is the standard provider. Next, we check for the existence of the attribute we are using in the provider. We also check for a hard-coded attribute called, data-focus.

The getBindings function does the same things as the previous function, by checking to see if we want to pass the standard binding attribute to the existing provider. After this, we call a helper function mapBindings which does the heavy lifting as far as creating the binding expression. Finally, we check to see if an data-focus attribute exists. If it does, we call the focus function on the node. This is a nice convention in that it allows us to define what element we want to set focus.

Let’s take a look at the mapBindings function. This function basically creates the binding statement for Knockout. We start with applying the valueUpdate binding.

Next we start evaluating all of the nodes we are going to support with this convention. For the anchor types, we simply create a click binding.

The button types follows this same convention but also creates a binding on enable property if a function exists with a prefix of ‘can’.

The next three conditions are straight forward and simply bind against the checked, value and text.

The last binding targets the image tag. This one is a little special in that it sets the binding against the attr and then the src attribute.

With each of these condition checks we see that we are using another helper function called, isBinding. This function takes a string and one of the arrays representing the elements. This function also uses a helper jQuery function inArray to determine if the string value is contained in the array.

Finally, the provider sets the conventionBindingProvider on the Knockout object.

All you need to do is reference this JavaScript file and then add the following code to your viewmodel:

 var ViewModel = function () {
    this.firstName = ko.observable("Matthew");
    this.middleName = ko.observable("Kevin");
    this.lastName = ko.observable("Duffield");
    this.password = ko.observable("");
    this.isActive = ko.observable(false);
    this.canrun = ko.computed(function () {
        return this.isActive();
    }, this);
    this.run = function () {
        alert("You hit run!");
    };
};

var options = { attribute: "data-name", valueUpdate: 'keyup' };
//tell Knockout that we want to use an alternate binding provider
ko.bindingProvider.instance = new ko.conventionBindingProvider(options);

ko.applyBindings(new ViewModel());​

The source for this provider is located here.

Play with the sample I created on jsFiddle and change the options parameter.

Hope this helps…

Advertisements
  1. No comments yet.
  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

%d bloggers like this: