A common source of discontent that I hear about Knockout.js is how easy it is to find yourself with complex and verbose data-bind
attributes that contain logic and code that belongs elsewhere.
Partially because of this issue, there has been interest lately in ways to unobtrusively apply bindings. If a robust solution is developed to handle this, then it may be an interesting option with its own pros and cons. However, I wanted to present another angle on this topic. I believe that there are many techniques that can be used to reduce the complexity and general craziness of the data-bind
attributes.
Here are some common examples of bindings that could stand to be improved for various reasons:
data-bind=”enable: items().length < 10”
data-bind=”visible: !hiddenFlag()”
data-bind=”text: selectedItem() ? selectedItem().name() : ‘unknown’”
data-bind=”value: name, event: { focus: function() { viewModel.selectItem($data); }, blur: function() { viewModel.selectItem(null); }”
data-bind=”css: { success: selectedItem().isSaved(), error: !selectedItem().isSaved() }”
data-bind=”click: function() { viewModel.selectItem($data); }”
Let’s look at some ways that we could handle each of these bindings in a cleaner way.
Encapsulating logic in a dependentObservable
Enabling or disabling an element based on a condition is a common scenario. It is not uncommon to see a binding that looks like:
1
|
|
This binding contains a JavaScript expression and it includes a a hard-coded value. We could improve this by pushing the logic to our view model in a computed observable.
1 2 3 4 5 6 7 8 |
|
Now, we can replace the binding with:
1
|
|
The item limit can now even be adjusted on-the-fly and the UI will react appropriately. This concept is also now represented in the view model and can potentially be reused in this view or other views.
Simple custom bindings that wrap existing bindings
It is not uncommon for the view model to contain a boolean that we want to pass to a binding. However, we frequently want to pass the opposite of that boolean based on how the particular binding is coded.
1
|
|
Besides changing our data model, one choice in this situation would be to write a computed observable that represents the opposite of this value. However, this may add unnecessary bloat to the view model. Instead, we can easily write a hidden binding that will allow you to pass hiddenFlag directly.
1 2 3 4 5 6 |
|
Now, the binding is simplified to:
1
|
|
Protecting against null objects
If you have an observable that contains an object and you want to bind to properties of that object, then you need to be careful if there is a chance that it can be null or undefined. You may write your binding like:
1
|
|
There are a number of ways to handle this one. The preferred way would be to simply use the template binding:
1 2 3 4 |
|
1 2 3 4 5 6 7 |
|
With this method, if selectedItem
is null, then it just won’t render anything. So, you would not see unknown as you would have in the original binding. However, it does have the added benefit of simplifying your bindings, as you can now just specify your property names directly rather than selectedItem().name
. This is the easiest solution.
Just for the sake of exploring some options, here are a few alternatives:
You could use a computed observable, as we did before.
1 2 3 4 |
|
However, this again adds some bloat to our view model that we may not want and we might have to repeat this for many properties.
You could use a custom binding like:
1
|
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Is this better than the original? I would say probably not. It does avoid the JavaScript in our binding, but it is still pretty verbose.
One other option would be to create an augmented observable that provides a safe way to access properties while still allowing the actual value to be null. Could look like:
1 2 3 4 5 6 7 8 |
|
So, this is just an observable that also exposes a computed observable named safe
that will always return an empty object, but the actual observable can continue to store null.
Now, you could bind to it like:
1
|
|
You would not see the unknown value when it is null, but it at least would not cause an error when selectedItem
is null.
I do think that the preferred option would be using the template binding in this case, especially if you have many of these properties to bind against.
Handling multiple events in a custom binding
When attaching methods from our view model to events on our elements, it is not uncommon for the syntax to get unruly, especially if we need to use an anonymous function to pass parameters.
1
|
|
In this case, we want to mark this item as selected when someone enters this field and clear the selection when they leave it.
This would be a good spot for a custom binding handler. In fact, the documentation already shows an example of creating a binding that will do this for you and it even supports updates from the selectedItem
being set or cleared programmatically.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
This would allow you to simplify the binding down to:
1
|
|
Returning a binding object from our view model
1 2 |
|
In this case, we want to add a success
class when isSaved
is true and an error
class when it is false.
One option is to use a computed observable that returns our binding object like:
1 2 3 4 5 6 7 8 9 10 11 |
|
Now, our css binding will always be given a proper object to bind against. In this case, we will likely be sending and receiving the selectedItem
itself or the items array, so having this selectedSaveClass
on our view model doesn’t interfere with the JSON that we communicate to/from the server. However, in the case that it does, there are a few ways to deal with it. You could override the toJSON()
method on your object as described here. Another trick is to place the properties that you don’t want serialized to JSON as sub-observables. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Now, if you do a ko.toJSON(viewModel)
or ko.toJSON(selectedItem)
, you will just get your item and not the style object. The toJSON()
method will see that selectedItem
is an observable and then unwrap it. It does not look for any properties/observables attached to the observable itself (which is a function). So, this is a nice way to hide values that are not important to send back to the server.
Avoiding anonymous functions in event bindings
update 5/1/2012: in KO 2.0, the data is now passed automatically to click
and event
handlers as the first argument and the parent scopes are available via $root
and $parent
. An anonymous function is generally no longer needed for this scenario.
Inside of a template, a common task is to call a method off of a parent object and pass the child object. In Knockout, to pass arguments to a function you need to wrap it in an anonymous function like:
1
|
|
There are several approaches that we could use to clean this up.
We could create a custom binding that automatically passes the data. Something like:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
If viewModel has global scope, then in your item you can now avoid the anonymous function like:
1
|
|
If it does not have global scope, then you can pass the function into your child template using the templateOptions parameter.
1 2 3 4 5 6 7 8 |
|
Another option that works quite well is to put the selection method on the item itself.
1 2 3 4 5 6 7 8 9 10 11 |
|
So, our item does not even need a parent property, we just pass the selectedItem
observable into the constructor function and use it inside of the select function. Now, you can simply do:
1
|
|
In my opinion, this is the simplest and cleanest option. However, it does take some planning in how the view model is populated, so the selectedItem
observable is available to be passed to our Item
constructor function.
Final Thoughts
Producing a clean view in Knockout is not always a simple task. It does take some planning and extra considerations in the view model. Custom bindings can make a big difference in simplifying your logic and in many cases they may need to be tailored towards performing a specific task to keep a clean syntax. In the end, you can end up with a clean view, a robust view model, and an array of bindings that can potentially be reused across a site.