Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

KO 1.3 Preview Part 2: Custom Binding Providers

| Comments

Update: Knockout 1.3 was eventually renamed to version 2.0

I am really excited about the changes coming in Knockout 1.3. In the previous post, I discussed the native template engine. It will likely change the way that we all write Knockout apps. The next feature that I find very intriguing is the ability to swap in a custom binding provider.

What is a binding provider?

To understand what a binding provider is all about, let’s talk about the default binding provider that comes with Knockout 1.3. The default binding provider looks for any elements that have a data-bind attribute. When the binding provider finds an element with this attribute, then it will parse the attribute value and turn it into a binding object using the current data context. This works in the same way as bindings have always worked in Knockout. You declaratively add your bindings to the elements that you want and Knockout binds them to the current data at that level.

What does a custom binding provider look like?

A binding provider only has to answer two simple questions:

  1. Given a DOM node, does that node have any bindings?
  2. Given a DOM node (that passed #1) and the current data context, what does the binding object look like?

A binding provider is an object that implements two functions:

1
2
3
4
5
6
7
8
9
var yourBindingProvider = {
    nodeHasBindings: function(node) {
        //return true/false
    },

    getBindings: function(node, bindingContext) {
        //return a binding object
    }
};

The nodeHasBindings function takes in a DOM node. Remember that this is a node, so it is not necessarily an element. In fact, the default binding provider looks at comment nodes to see if they are containerless bindings.

The getBindings function expects you to return an object that represents the bindings as applied to the current data context. For example, if you want to apply a text binding with the value of the current data’s name observable, then you would return an object like: { text: bindingContext.$data.name }.

Writing a quick custom binding provider

Suppose that we are not satisfied with putting our logic as strings in the data-bind attributes. Maybe our bindings are getting too verbose or they contain more logic than we want in our view. What if we did something similar to CSS classes and explicitly assigned bindings by name to our elements? So, we don’t confuse our presentation classes and our data classes, I am going to assign our binding classes in a data-class attribute.

Our nodeHasBindings function can be pretty simple:

1
2
3
4
//determine if an element has any bindings
function nodeHasBindings(node) {
    return node.getAttribute ? node.getAttribute("data-class") : false;
};

If the node supports the getAttribute function (it is an element), then see if the data-class attribute has a value.

Now we need to write the getBindings function. Like CSS classes, it would be nice to support multiple space-separated classes in a single data-class attribute. This could allow us to share binding specifications between elements.

First let’s take a look at what our bindings will look like. We are going to create an object to hold the bindings. The property names will match the keys that we will use in the data-class attributes. Knockout uses a with statement when parsing bindings that allows variables to be evaluated with the current data context at the top of the scope chain. For these bindings, let’s avoid using with and instead rely on this being set to the current data context when the binding object is returned.

Here are some sample bindings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//our binding object
var bindings = {
    products: {
        foreach: viewModel.products
    },
    nameText: function() {
        return {
            text: this.name
        };
    },
    selectLink: function() {
        return {
            visible: !this.isSelected(),
            click: this.select
        };
    },
};

In the products binding, we can simply return the binding object, as the observableArray that we care about is directly on our view model. In that case, we do not need to rely on this being set correctly. For the other bindings, we return the binding object in an anonymous function where we rely on this being set to the appropriate data. Now we have our bindings specified and we didn’t even need to put them in strings.

The next step is to write our getBindings function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//return the bindings given a node and the bindingContext
function getBindings(node, bindingContext) {
    var result = {};
    var classes = node.getAttribute("data-class");
    if (classes) {
        classes = classes.split(' ');
        //evaluate each class, build a single object to return
        for (var i = 0, j = classes.length; i < j; i++) {
            var bindingAccessor = this.bindingObject[classes[i]];
            if (bindingAccessor) {
                var binding = typeof bindingAccessor == "function" ? bindingAccessor.call(bindingContext.$data) : bindingAccessor;
                ko.utils.extend(result, binding);
            }
        }
    }

    return result;
};

We loop through each key in the data-class attribute and build up a result object from each one. If the binding object is a function, then we call it with our current data as this, otherwise we just use the object that was provided. Currently this handler does not consider the containerless bindings supported in 1.3 that live in comment nodes, but that could be added with some additional work.

Here is what the completed binding provider looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//You can now create a bindingProvider that uses something different than data-bind attributes
ko.customBindingProvider = function(bindingObject) {
    this.bindingObject = bindingObject;

    //determine if an element has any bindings
    this.nodeHasBindings = function(node) {
        return node.getAttribute ? node.getAttribute("data-class") : false;
    };

    //return the bindings given a node and the bindingContext
    this.getBindings = function(node, bindingContext) {
        var result = {};
        var classes = node.getAttribute("data-class");
        if (classes) {
            classes = classes.split(' ');
            //evaluate each class, build a single object to return
            for (var i = 0, j = classes.length; i < j; i++) {
               var bindingAccessor = this.bindingObject[classes[i]];
               if (bindingAccessor) {
                   var binding = typeof bindingAccessor == "function" ? bindingAccessor.call(bindingContext.$data) : bindingAccessor;
                   ko.utils.extend(result, binding);
               }
            }
        }

        return result;
    };
};

The constructor for our binding handler takes in the object used to look up our binidngs. Now, the final step is to tell Knockout to use our custom binding provider (before calling ko.applyBindings) by overriding ko.bindingProvider.instance like this:

1
2
//set ko's current bindingProvider equal to our new binding provider
ko.bindingProvider.instance = new ko.customBindingProvider(bindings);

Now, we can specify bindings in our HTML like:

1
2
3
4
5
6
7
8
9
<div id="list">
    <h2>Product</h2>
    <ul data-class="products">
        <li>
            <a href="#" data-class="nameText selectLink"></a>
            <span class="selected" data-class="nameText selectSpan"></span>
        </li>
    </ul>
</div>

This binding provider sample implementation is available on Github here https://github.com/rniemeyer/knockout-classBindingProvider.

Here is a sample using this technique:

Link to sample on jsFiddle.net

Is this better than the default binding provider?

I personally prefer using declarative bindings in the data-bind attribute, but I think that it is great to have the flexibility to do it in a different way. Even with a technique like this, I still think that it is wise to write a clean and robust view model, possibly using some of the ideas from this post. The view model itself should not change either way. If using a custom binding handler like this one, it is a good idea to keep the object that holds the bindings cleanly separated from the view model.

What else could you do with a custom binding provider?

While the default binding mechanism in Knockout works well for most of us, some developers are looking for alternative ways of specifying their bindings. Some different possibilities for custom binding providers:

  • Use id/class attributes to find bindings in a binding object. Maybe something like Brandom Satrom’s excellent Knockout.Unobtrusive plugin.
  • Store bindings in jQuery $.data or expando properties on the elements (ko.utils.domData.get/set). This would allow you to add bindings directly in code that write to this location. A jQuery plugin could be used to apply bindings to a set of elements.
  • use more specific attributes like data-bind-text and build the bindings based on the attribute name (like Aaron Powell’s KO pre-parser).
  • maybe something with conditional bindings. Based on some state, bindings turn on/off.
  • maybe namespaced bindings where the actual view model applied to the binding depends on the attribute name (like knockout.namespaces).

Comments