One of my most popular Open source projects, FreePIE, is a WPF application and it utilizes a library called Caliburn.Micro. The main idea for that library is to minimize the use of explicit declared bindings and use conventions to implicit bind to the ViewModel under the hood. As an example, if you have a button called Save and a method called Save on your VM, Caliburn.Micro will make sure these are bound.
This is a welcomed tool since XAML bindings are, to say the least, verbose.
Bindings get even more verbose in the Knockout world because of the fact that you can write inline JavaScript in the bindings, fact is the entire data-bind attribute is inline JavaScript that is executed by Knockout. I’ve seen that bindings like these and worse are common out there.
<button data-bind="enable: errors().length === 0">Save</button>
I started to write a Convention based library called Knockout.BindingConventions a year ago and I added support for Knockout 3.0 a while back. Even though the library isn’t new I thought I could write about it, it’s a good way to get to know the inner working of Knockout and also convention over configuration.
There are two main things a library like this require, first it needs to be able to hook into the binding provider part of Knockout and it also needs to provide a good API to define conventions.
Binding provider
Knockout is built like a modular library so it’s very easy to hook into its different parts. To override the binding provider you just implement an object with these public members.
ko.bindingConventions.ConventionBindingProvider.prototype = { nodeHasBindings: function (node) { }, getBindingAccessors: function (node, bindingContext) { } };
nodeHasBindings returns true if a data-name or data-bind attribute is present, data-name is a special convention based binding attribute introduced by this library. If nodeHasBindings returns true Knockout will call getBindingAccessors which in turn will return an object with all the bindings that knockout should apply.
getBindingAccessors first needs to resolve the ViewModel member context of the name supplied in the data-name attribute. It’s a bit complex but its done like this.
var data = bindingContext[name] ? bindingContext[name] : bindingContext.$data[name]; var context = bindingContext.$data; if (data === undefined) { var result = getDataFromComplexObjectQuery(name, context); data = result.data; bindingContext = { $data: result.context }; name = result.name; } if (data === undefined) { throw "Can't resolve member: " + name; } var dataFn = function() { return data; };
We first simply try to get the data context by indexing the bindingContext provided by Knockout. That should provide results most of the time. If that fails we check if the name provided is a bit more complex like “parent.child”.
If that fails too we throw an exception so that the library does not quietly swallow that the data-name attribute is not an actual member on the data context. We also need to override the default binding provider with our provider.
ko.bindingProvider.instance = new ko.bindingConventions.ConventionBindingProvider();
Convention API
Now that we have the data context we just need to iterate over our conventions and see if any fits the description. For this we need a API so that dev’s can add their own convention or modify the built-in ones. To create a convention you add an object literal to ko.bindingConventions.conventionBinders. The object literal look like this.
{ rules: [], apply: function (name, element, bindings, unwrapped, type, dataFn, bindingContext) { } };
The rules array should contain functions, if all functions return true we apply the convention using the apply function. This way its easy for dev’s to add or remove rules to the built-in conventions.
The Button convention is a good and simple example on a convention.
ko.bindingConventions.conventionBinders.button = { rules: [function (name, element, bindings, unwrapped, type) { return element.tagName === "BUTTON" && type === "function"; } ], apply: function (name, element, bindings, unwrapped, type, dataFn, bindingContext) { bindings.click = dataFn; setBinding(bindings, 'enable', "can" + getPascalCased(name), bindingContext); } };
The rule function check that the element is of type “BUTTON” and that the supplied data context is a function. It’s safe to assume that said function is a click handler.
The apply function simply applies a click binding using the dataFn supplied by the library. We also want the library to check if there is a guard member on the context. If you have a save function on the model and a corresponding canSave member then we want that hooked up to the enable binding. First we get the camel cased name of the member using a helper function getPascalCased then we prepend it with “can”. setBinding is a helper function that will apply the binding if it can find the member on the bindingContext. There is a util closure, ko.bindingConventions.utils, both setBinding and getPascalCased are exported here so that dev’s can use them from their own conventions.
Template convention
One of the coolest features with Caliburn.Micro is that it can by convention connect ViewModels with views. If you have a ViewModel called MyViewModel it will understand that it should connect it to the view called MyView. In the C# world this is easy because of the static typed nature of the language. In JavaScript it’s not as easy! But I came up with a way that works pretty well. Before you apply knockout you must first call a init function that takes an object literal with options.
ko.bindingConventions.init({ roots: [MyApp] });
One of those options is a roots array, here you can specify all your closures for your application. This way when the template convention finds a model reference it can iterate over the closures until it finds model and then use its member name on its parent closure to determine the name of the model. I have extracted the functionally to a helper function that’s exported to the util closure, ko.bindingConventions.utils.findConstructorName.
Head over to the template convention wiki for a JSFiddle demo
Built in conventions
Install using nuget
Install-Package Knockout.BindingConventions
Source code can be found here.