Wednesday, February 1, 2012

Exploring TodoMVC and knockout.js with unobtrusive bindings

There’s an interesting project I’ve been keeping an eye on over at github called TodoMVC“a project which offers the same Todo application implemented using MVC concepts in most of the popular JavaScript MV* frameworks of today”.

I’m also a bit of a sucker for knockout.js which aims to “simplify dynamic JavaScript UIs by applying the Model-View-ViewModel(MVVM) pattern”. I haven’t used MVVM extensively before, having only dabbled in WPF/Silverlight, so I’m coming at knockout.js without any preconceived notions of any sort. Plus it has great documentation and examples…

To begin with, I thought I would create a JSFiddle based directly on the knockout.js version of TodoMVC. Really very easy to setup, but nice to be able to play around with.

One of the main complaints levelled at knockout.js is the apparent lack of Separation of Concerns regarding how bindings are managed via data-bind attributes, e.g.

1 <p>First name: <input data-bind="value: firstName" /></p>

This didn’t particularly bother me at first but when dealing with lots of elements/attributes it starts to get more difficult to manage the bindings in the html itself. Turns out knockout.js 1.3+ supports custom binding providers that allow us to refactor the above code into something like (example code only):

1 <p>First name: <input data-class="firstName" /></p> 2 3 <script type="text/javascript" > 4 var viewModel = {name: "Ichigo"}; 5 var bindings = {firstName: {value:} }; 6 ko.bindingProvider.instance = new ko.customBindingProvider(bindings); 7 ko.applyBindings(viewModel); 8 </script>

It might seem like a bit more work to begin with, but now we have true separation between our model and our UI. It also allows much greater flexibility when dealing with complicated UIs.

With this information in hand I’ve decided to port the knockout.js version of TodoMVC to use unobtrusive bindings. There’s surprisingly little work in the conversion process, it mostly involves just pulling all the data-bind attributes out the html and replacing them with data-class attributes that correspond to the properties of the bindings object. 

1 var bindings = { 2 3 newTodo: { 4 value: viewModel.current, 5 valueUpdate: 'afterkeydown', 6 enterKey: viewModel.add 7 }, 8 taskTooltip : { visible: viewModel.showTooltip }, 9 checkAllContainer : {visible: viewModel.todos().length }, 10 checkAll: {checked: viewModel.allCompleted }, 11 12 todos: {foreach: viewModel.todos }, 13 todoListItem: function() { return { css: { editing: this.editing } }; }, 14 todoListItemWrapper: function() { return { css: { done: this.done } }; }, 15 todoCheckBox: function() {return { checked: this.done }; }, 16 todoContent: function() { return { 17 text: this.content, 18 event: { dblclick: this.edit } };}, 19 todoDestroy: function() {return { click: viewModel.remove };}, 20 21 todoEdit: function() { return { 22 value: this.content, 23 valueUpdate: 'afterkeydown', 24 enterKey: this.stopEditing, 25 event: { blur: this.stopEditing } }; }, 26 27 todoCount: {visible: viewModel.remainingCount}, 28 remainingCount: { text: viewModel.remainingCount }, 29 remainingCountWord: function() { return { 30 text: viewModel.getLabel(viewModel.remainingCount) };}, 31 32 todoClear: {visible: viewModel.completedCount}, 33 todoClearAll: {click: viewModel.removeCompleted}, 34 completedCount: { text: viewModel.completedCount }, 35 completedCountWord: function() { return { 36 text: viewModel.getLabel(viewModel.completedCount) }; }, 37 38 todoInstructions: {visible: viewModel.todos().length} 39 };

The important part to note out of this is that you have to specify the context of the value/function you want your data-bound attribute to link to. This is relatively easy for top-level values/functions as you just need to reference them via the viewModel object.

In order for binding to work on individual items (e.g. todo tasks in viewModel.todos), you need to wrap the bindings in a closure and reference “this” instead of the viewModel directly.

One caveat to this is that you also need to provide a closure when executing viewModel functions within your bindings (see line 30 and 36 above), otherwise the attributes will always bind to the result of the function when it was first called. Not a particularly easy problem to track down if you haven’t been bitten by closure bugs before.

Anyway checkout the following jsfiddle to explore the full example.