Custom bindings are a powerful and exciting feature that seem to be often overlooked by newcomers to Knockout.js. They are the key to allowing Knockout to gracefully handle complex behaviors and/or control third-party components/widgets. While there is good documentation on custom bindings here, I wanted to provide an additional perspective on them.
It is easy to get hung up on the custom part of the name custom binding and feel like you are making up for some short-coming in the framework or that it is a “heavyweight” or “complex” solution to a problem that seems simple. However, I feel that custom bindings should be treated as just another tool in your KO toolbox, along with computed observables and manual subscriptions, rather than a last resort that is only turned to after failing to make the default bindings work for a scenario.
In my ideal Knockout application, all of the code that involves accessing and manipulating the DOM in any way is isolated to bindings. So, anything that crosses between your view model and your UI can (and should) be in a binding. Some bindings may not even involve your view model and can just provide a way to declaratively wire up some functionality.
The mechanics of custom bindings
When you define a custom binding, you are able to specify an
init and an
1 2 3 4 5 6 7 8
init function will only run the first time that the binding is evaluated for this element. This is usually used to run one-time initialization code or to wire up event handlers that let you update your view model based on an event being triggered in your UI.
update function provides a way to respond when associated observables are modified. Typically, this is used to update your UI based on changes to your view model. It is important to understand the three cases where the update function can be called:
- Initially when the binding is first evaluated (after the init function)
- Any time that an observable changes that was accessed as part of the the previous execution of the update function for this binding. Bindings are implemented internally using computed observables, so any observables that have their value accessed create dependencies for the binding.
- Any time that another binding in the same data-bind attribute is triggered. This helps ensure things like the value is appropriate when the options are changed.
update functions are supplied four arguments. Generally, you will want to focus on the element and the valueAccessor parameters, as they are the standard way to link your view model to your UI.
- element - this gives you direct access to the DOM element that contains the binding. In this paradigm, you have no real need to give these elements ids, names, or classes for the purpose of selecting/locating them (unless you need to for other reasons).
- valueAccessor - this is a function that gives you access to what was passed to the binding. If you passed an observable, then the result of this function will be that observable (not the value of it). If you used an expression in the binding, then the result of the valueAccessor will be the result of the expression.
- allBindingsAccessor - this gives you access to all of the other bindings that were listed in the same data-bind attribute. This is generally used to access other bindings that interact with this binding. These bindings likely will not have any code associated with them and are just a way to pass additional options to the binding, unless you choose to pass an object with multiple properties into your main binding. For example,
optionsCaptionare bindings that are only used to pass options to the
- data - For bindings outside of templates, this will provide access to your overall view model. Inside of a template, this will be set to the data being bound to the template. For example, when using the foreach option of the template binding, the viewModel parameter would be set to the current array member being sent through the template. Most of the time the valueAccessor will give you the data that you want, but the viewModel parameter is particularly useful if you need an object to be your target when you call/apply functions.
- context - This is the binding context which includes properties like
$root. Documentation about each property available can be found here.
1- Simple initialization
In a case where you just want to declaratively run some code against an element, you can use a simple init. For example, if you want to hook up a jQuery UI button, it might look like:
1 2 3 4 5 6
You would declare it like:
Sample here: http://jsfiddle.net/rniemeyer/Rn9tg/
2- Run some code against an element when an observable changes
Suppose we want to make an element flash whenever a certain observable changes. We could do something like:
1 2 3 4 5 6
Notice that we try to unwrap the value passed to us, in case it is an observable, but we do not even keep track of the value. This is simply to create a dependency in our binding on that observable.
Sample here: http://jsfiddle.net/rniemeyer/s3QTU/
3- Simple wrapper binding
One of the easiest types of binding to start with is one that simply “wraps” an existing binding. For example, suppose you want some text to fade in when it changes. The
text binding already handles updating the element properly (and is presumably well-tested against various browsers) and we just want to add some animation to it. So, we can write a simple wrapper to the text binding that looks like:
1 2 3 4 5 6 7
Sample here: http://jsfiddle.net/rniemeyer/SmkpZ/
4- Wrapper that forwards a modified valueAccessor
Another example of a “wrapper” binding is when you want to intercept the value passed to the binding and modify it before passing it on to a default binding. Suppose, we want to create a modified click binding that allows us to pass an array of parameters to it, rather than being forced to use an anonymous function.
1 2 3 4 5 6 7 8 9 10 11
You would then use this by passing a function as the
action property and an array of parameters as the
params property of the object passed to the binding. The binding then builds a new valueAccessor parameter to pass to the click binding.
Sample here: http://jsfiddle.net/rniemeyer/NkqmK/
5- Two-way binding that needs to broker getting/setting data with a 3rd party control
Suppose we want to bind to a third-party control that exposes APIs for reading and writing values to it. For example, the jQuery UI datepicker control allows us to set its value using a Date object and retrieve that Date object from it rather than dealing with strings.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
You would use the binding like:
This custom binding handles three aspects of working with the jQuery UI datepicker control:
- initializing the control (with optional config options for the widget)
- responding to updates made in the UI by setting up an event handler
- responding to updates made to the view model.
These are the typical scenarios that would need to be handled when dealing with an editable control.
Sample here: http://jsfiddle.net/rniemeyer/X82aC/
A quick note about bindings that write to non-observables
Knockout supports binding editable controls to non-observables (plain properties). In this case, edits to the field do write to the underlying values, but nobody is notified. To properly set a non-observable property on your view model, Knockout provides a setter function via an additional binding, as it would not have a way to pass the property by reference. This is only available for primitive types. For a binding that requires multiple options, if you need to support writing to a non-observable, then this may be a reason to provide options by additional bindings rather than by passing an object literal.
Custom bindings make Knockout.js capable of easily and elegantly controlling complex behaviors. While it is great to create generic, reusable bindings, they can also be used to just simplify your code by combining multiple actions into a single binding or setting default options/logic in a binding rather than using overly complicated data-bind attributes. Once you start writing custom bindings, you will quickly see that they unlock a whole new world of possibilities!