let: binding for Knockout
Posted on Wed 18 November 2015 in Code
Knockout is a JavaScript “framework” that has always lurked in the shadows of other, more flashy ones. Even in the days of its relative novelty, the likes of Backbone or Ember had seemed to garner more widespread interest among frontend developers. Today, this hasn’t changed much, the difference being mainly that the spotlight is now taken by new actors (*cough* React *cough*).
But Knockout has a loyal following, and for good reasons. Possibly the most imporant one is why I’ve put the word “framework” in quotes. Knockout is, first and foremost, a data binding library: it doesn’t do much else besides tying DOM nodes to JavaScript objects.
This quality makes it both easy to learn and simple to integrate. In fact, it can very well live in just some small compartments of your web application, mingling easily with any server-side templating mechanism you might be using. It also interplays effortlessly with other JS libraries, and sticks very well to whatever duct tape you use to hold your frontend stack together.
Lastly, it’s also quite extensible. We can, for example, create our own bindings rather easily, extending the declarative language used to describe relationship between the data and UI.
In this post, I’m going to demonstrate this by implementing a very simple let:
binding —
a kind of “assignment” of an expression to a name.
From Knockout with:
bluff
Out of box, Knockout proffers the with:
binding,
a quite similar mechanism. How it may be somewhat problematic is analogous to the
widely discouraged with
statement
in JavaScript itself. Namely, it blends several namespaces together, making it harder to determine which object
is being referred to. As a result, the code is more prone to errors.
On the other hand, freeing the developer from repeating long and complicated expressions is obviously valuable.
Perhaps reducing them to nil is not the right approach, though, so how about we just shorten them
to a more manageable length? Well, that’s exactly what the let:
binding is meant to do:
<div data-bind="let: { addr: currentUser.personalInfo.address }">
<p data-bind="text: addr.line1"></p>
<!-- ko if: addr.line2 -->
<p data-bind="text: addr.line2"></p>
<!-- /ko -->
<p>
<span data-bind="text: add.city"></span>,
<span data-bind="text: add.region"></span>
</p>
<p data-bind="text: add.country"></p>
</div>
Making it happen turns out to be pretty easy.
Binding contract
To define a Knockout binding, up to two things are needed. We have to specify what the library should do:
- when the binding is first applied to a DOM node (the
init
method) - when any of the observed values changes (the
update
method)
Not every binding has to implement both methods. In our cases, only the init
is necessary, because all we have to do
is modify the binding context.
What’s that? Shortly speaking, a binding context,
is an object holding all the data you can potentially bind to your DOM nodes. Think of it as a namespace, or local scope:
whatever’s in there can be used directly inside data-bind
attributes.
let:
it go
Therefore, all that the let:
binding has to do is to extend the context with a mapping passed to it as an argument.
Well, almost all:
ko.bindingHandlers['let'] = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var innerContext = bindingContext.extend(valueAccessor);
ko.applyBindingsToDescendants(innerContext, element);
return { controlsDescendantBindings: true };
}
};
The resulting innerContext
is a copy of the original bindingContext
, augmented with additional properties from
the argument of let:
(those are available through valueAccessor
). Once we have it, though, we need to handle it
in a little special way.
Normally, Knockout is processing all bindings recursively, pasing down the same bindingContext
(which ultimately
comes from the root viewModel
). But since we want to locally alter the context, we also need to interrupt this regular
way and take care of the lower-level DOM nodes ourselves.
This is exactly what the overly-long ko.applyBindingsToDescendants
function is doing. The only caveat is that Knockout
has to be told explicitly
about our intentions through the return
value from init
. Otherwise, it would try to apply the original
bindingContext
recursively, which in our case would amount to applying it twice.
{ controlsDescendantBindings: true }
prevents Knockout from doing so erroneously.