Unobtrusive databinding in Knockout JS
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:
- It mixes markup and code - databinding shouldn't be expressed in markup, in my opinion (separation of concerns);
- It bloats the markup; and
- It can be a laborious task, adding all of the bindings.
<p>First name: <input data-bind="value: firstName" /></p>Figure 1
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"></span>!</h2>
Wouldn't it be easier and cleaner if Knockout JS was able to bind automatically? For example:
<p>First name: <input name="firstName" /></p>Figure 2
<p>Last name: <input name="lastName" /></p>
<h2>Hello, <span id="fullName"></span>!</h2>
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:
- Using the
id
,name
(for<input>
or<select>
tags) or aclass
name, attempts to map to a member of the current$data
object; and - Based on the HTML element type and the type of the member, determines what binding should be used (see Table 1).
Table 1
HTML element Member type Binding * Number, String text * Boolean visible * Object, Observable with * Array, ObservableArray foreach * Function click input Number, String, Observable value input[type="checkbox|radio"] Boolean checked select Object, Number, String, Observable value select Array, ObservableArray selectedOptions
- 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
orclass
) 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>Figure 4
this.hairColours = ko.observableArray(["Black","Blonde","Brunette","Grey","Red","White"]);
this.hairColour = ko.observable("Blonde").extend({ bindings: "options:hairColours" });
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>Figure 5
this.hairColours = ko.utils.extend([...], { bindings: "attr:{title:'colours'}" });
this.sayHello = ko.utils.extend(function(){...}, { bindings: "enable:firstName() && lastName()" });
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.