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.
Get Learning JavaScript Design Patterns now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.