0 Comments

Introduction

I've been using Knockout JS for quite a few years and it's a great that library that 'helps you to create rich, responsive display and editor user interfaces with a clean underlying data model.'[1] However, one thing I really dislike is the use of the data-bind attribute (see Figure 1), for three reasons:

  1. It mixes markup and code - databinding shouldn't be expressed in markup, in my opinion (separation of concerns);
  2. It bloats the markup; and
  3. It can be a laborious task, adding all of the bindings.
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"></span>!</h2>
Figure 1

Wouldn't it be easier and cleaner if Knockout JS was able to bind automatically? For example:

<p>First name: <input name="firstName" /></p>
<p>Last name: <input name="lastName" /></p>
<h2>Hello, <span id="fullName"></span>!</h2>
Figure 2

The knockout.unobtrusiveBindingProvider

"knockout.unobtrusiveBindingProvider is an unobtrusive, convention-based binding provider for Knockout JS that enables a clean separation of HTML and Knockout databindings."[2]

How does it work? As Knockout traverses the DOM, the unobtrusiveBindingProvider analyses each HTML element and:

  1. Using the id, name (for <input> or <select> tags) or a class name, attempts to map to a member of the current $data object; and
  2. Based on the HTML element type and the type of the member, determines what binding should be used (see Table 1).
HTML elementMember typeBinding
*Number, Stringtext
*Booleanvisible
*Object, Observablewith
*Array, ObservableArrayforeach
*Functionclick
inputNumber, String, Observablevalue
input[type="checkbox|radio"]Booleanchecked
selectObject, Number, String, Observablevalue
selectArray, ObservableArrayselectedOptions
Table 1
  • If the appropriate member isn't found, in the current context, the unobtrusiveBindingProvider will attempt to find one by bubbling up through the parent contexts.
  • Where an element has multiple classes and a member hasn't already been mapped, the unobtrusiveBindingProvider will attempt to map each class until a member has been found.
  • If the target (id, name or class) is hyphonated and a member hasn't already been mapped, then the unobtrusiveBindingProvider will regard this as a path to a member in the object graph and will attempt to navigate through the object graph until it reaches the destination member. However, if any part of the hyphonated target doesn't match a path in the object graph, the mapping will be terminated. If any part of the path is an observable then the parentheses should be omitted.

But what happens if we don't want to use the binding that is mapped in Table 1?

The binding extender

In the scenario where you need to use a different binding the unobtrusiveBindingProvider includes a binding extender (see Figure 3), which will override the mapping.

this.firstName = ko.observable("Bob").extend({ binding: "textInput" });
Figure 3

The bindings extender

Quite often we need to declare additional bindings. The unobtrusiveBindingProvider enables this through the use of the bindings extender. For example:

<select name="hairColour"></select>

this.hairColours = ko.observableArray(["Black","Blonde","Brunette","Grey","Red","White"]);
this.hairColour = ko.observable("Blonde").extend({ bindings: "options:hairColours" });
Figure 4

Binding Arrays and Functions

If the member of the model is an array or function then the ko.utils.extend method can be used to add bindings. For example:

<button id="sayHello">Say Hello</button>

this.hairColours = ko.utils.extend([...], { bindings: "attr:{title:'colours'}" });
this.sayHello = ko.utils.extend(function(){...}, { bindings: "enable:firstName() && lastName()" });
Figure 5

The binding of a function can be overridden with an event name. For example:

this.sayHello = ko.utils.extend(function(){...}, { binding: "mouseover" });
Figure 6

The ignore extender

Where the id, name or a class matches a member but the member shouldn't be bound to the view, the unobtrusiveBindingProvider includes an ignore extender. For example:

this.fullName = ko.pureComputed(...).extend({ ignore: true });
Figure 7

The ko.bindings property

Views will often declare bindings that don't map to a member of the model. In this scenario, the unobtrusiveBindingProvider will use a property, on the ko object, called bindings. This should be an object with property names that map to the id or a class of a HTML element. NB: The CSS convention, when a selector comprises multiple words, is to hyphonate the words. Bindings names can be hyphonated, as long as they are surrounded with quotation marks. For example:

ko.bindings = { "say-hello": "click:sayHello" };
Figure 8

Overriding model members

Bindings declared in the ko.bindings object can override bindings declared on a member. To do this the property must be assigned an object, which includes a bindings property and an override property (set to true). For example:

ko.binding = { "say-hello": { bindings: "click:sayGoodbye", override: true } };
Figure 9

The ko.debug property

One of the reasons for the unobtrusiveBindingProvider is to eliminate the data-bind attribute. However, it can be useful to see what the bindings are, during debugging. The ko.debug property allows us to define the conditions under which the data-bind attribute should be added. The ko.debug property is automatically set to true if the hostname is "localhost" or the protocol is "file:" but this can be overridden. For example:

ko.debug = location.hostname === "stevenbey.com";
Figure 10

Conclusion

The unobtrusiveBindingProvider enables you to maintain clean and simple HTML, whilst observing separation of concerns.

Nuget


Example