O'Reilly logo

Learning JavaScript Design Patterns by Addy Osmani

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

MVVM with Looser Data Bindings

It’s not uncommon for JavaScript developers from an MVC or MVP background to review MVVM and complain about its true separation of concerns. Namely, the quantity of inline data bindings maintained in the HTML markup of a View.

I must admit that when I first reviewed implementations of MVVM (e.g., KnockoutJS, Knockback), I was surprised that any developer would want to return to the days of old where we mixed logic (JavaScript) with our markup and found it quickly unmaintainable. The reality however is that MVVM does this for a number of good reasons (which we’ve covered), including facilitating designers to more easily bind to logic from their markup.

For the purists among us, you’ll be happy to know that we can now also greatly reduce how reliant we are on data bindings, thanks to a feature known as custom binding providers, introduced in KnockoutJS 1.3 and available in all versions since.

KnockoutJS by default has a data-binding provider, which searches for any elements with data-bind attributes on them such as in the below example.

<input id="new-todo" type="text" data-bind="value: current, valueUpdate: "afterkeydown", 
enterKey: add" placeholder="What needs to be done?"/>

When the provider locates an element with this attribute, it parses it and turns it into a binding object using the current data context. This is the way KnockoutJS has always worked, allowing us to declaratively add bindings to elements that KnockoutJS binds to the data at that layer.

Once we start building Views that are no longer trivial, we may end up with a large number of elements and attributes whose bindings in markup can become difficult to manage. With custom binding providers, however, this is no longer a problem.

A binding provider is primarily interested in two things:

  • When given a DOM node, does it contain any data bindings?

  • If the node passed this first question, what does the binding object look like in the current data context?

Binding providers implement two functions:

nodeHasBindings

This takes in a DOM node, which doesn’t necessarily have to be an element.

getbindings

This returns an object representing the bindings as applied to the current data context.

A skeleton binding provider might thus look as follows:

var ourBindingProvider = {
  nodeHasBindings: function( node ) {
      // returns true/false
  },

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

Before we get to fleshing out this provider, let’s briefly discuss logic in data bind attributes.

If when using Knockout’s MVVM you find yourself dissatisfied with the idea of application logic being overly tied into your View, we can change this. We could implement something a little like CSS classes to assign bindings by name to elements. Ryan Niemeyer has previously suggested using data-class for this to avoid confusing presentation classes with data classes, so let’s get our nodeHasBindings function supporting this:

// does an element have any bindings?
function nodeHasBindings( node ) {
    return node.getAttribute ? node.getAttribute("data-class") : false;
};

Next, we need a sensible getBindings() function. As we’re sticking with the idea of CSS classes, why not also consider supporting space-separated classes to allow us to share binding specs between different elements?

Let’s first review what our bindings will look like. We create an object to hold them where our property names need to match the keys we wish to use in our data classes.

There isn’t a great deal of work required to convert a KnockoutJS application from one that uses traditional data bindings to one with unobtrusive bindings by applying custom binding providers. We simply pull our all of our data-bind attributes, replace them with data-class attributes, and place our bindings in a binding object as per below:

var viewModel = new ViewModel( todos || [] ),
    bindings = {

        newTodo:  { 
            value: viewModel.current, 
            valueUpdate: "afterkeydown", 
            enterKey: viewModel.add 
        },

        taskTooltip : { visible: viewModel.showTooltip },
        checkAllContainer : {visible: viewModel.todos().length },
        checkAll: { checked: viewModel.allCompleted },

        todos: { foreach: viewModel.todos },
        todoListItem: function() { return { css: { editing: this.editing } }; },
        todoListItemWrapper: function() { 
          return { css: { done: this.done } }; 
        },
        todoCheckBox: function() { return { checked: this.done }; },
        todoContent: function() { return { text: this.content, event: 
        { dblclick: this.edit } }; },
        todoDestroy: function() { return { click: viewModel.remove }; },        

        todoEdit: function() { return {
            value: this.content, 
            valueUpdate: "afterkeydown", 
            enterKey: this.stopEditing, 
            event: { blur: this.stopEditing } }; 
        },

        todoCount: { visible: viewModel.remainingCount },
        remainingCount: { text: viewModel.remainingCount },
        remainingCountWord: function() { 
          return { text: viewModel.getLabel(viewModel.remainingCount) };
        },
        todoClear: {visible: viewModel.completedCount},
        todoClearAll: {click: viewModel.removeCompleted},
        completedCount: { text: viewModel.completedCount },
        completedCountWord: function() { 
          return { text: viewModel.getLabel(viewModel.completedCount) }; 
        },
        todoInstructions: { visible: viewModel.todos().length }
    };

    ....

There are however two lines missing from the above snippet: we still need our getBindings function, which will loop through each of the keys in our data-class attributes and build up the resulting object from each of them. If we detect that the binding object is a function, we call it with our current data using the context this. Our complete custom binding provider would look as follows:

    // We 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 = {},
            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;
    }; 
};

Thus, the final few lines of our bindings object can be defined as follows:

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

// bind a new instance of our ViewModel to the page
ko.applyBindings( viewModel );

})();

What we’re doing here is effectively defining a constructor for our binding handle, which accepts an object (bindings) that we use to lookup our bindings. We could then rewrite the markup for our application View using data classes as follows:

<div id="create-todo">
                <input id="new-todo" data-class="newTodo" placeholder=
                "What needs to be done?" />
                <span class="ui-tooltip-top" data-class="taskTooltip" style=
                "display: none;">
                Press Enter to save this task</span>
            </div>
            <div id="todos">
                <div data-class="checkAllContainer" >
                    <input id="check-all" class="check" type="checkbox" data-class=
                    "checkAll" />
                    <label for="check-all">Mark all as complete</label>
                </div>
                <ul id="todo-list" data-class="todos" >
                    <li data-class="todoListItem" >
                        <div class="todo" data-class="todoListItemWrapper" >
                            <div class="display">
                                <input class="check" type="checkbox" data-class=
                                "todoCheckBox" />
                                <div class="todo-content" data-class="todoContent" 
                                style="cursor: pointer;"></div>
                                <span class="todo-destroy" data-class=
                                "todoDestroy"></span>
                            </div>
                            <div class="edit'>
                                <input class="todo-input" data-class="todoEdit'/>
                            </div>
                        </div>
                    </li>
                </ul>
            </div>

Neil Kerkin has put together a complete TodoMVC demo app using the above, which you can access and play around with here.

While it may look like quite a lot of work in the explanation above, now that we have a generic getBindings method written, it’s a lot more trivial to simply reuse it and use data classes rather than strict data bindings for writing our KnockoutJS applications instead. The net result is hopefully cleaner markup with our data bindings being shifted from the View to a bindings object instead.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required