Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Simplifying and Cleaning Up Views in KnockoutJS

| Comments

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
<button data-bind="click: addItem, enable: items().length < 10">Add Item</button>

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
var viewModel = {
  items: ko.observableArray(),
  itemLimit: ko.observable(10)
};

viewModel.addItemsAllowed = ko.computed(function() {
  return this.items().length < this.itemLimit();
}, viewModel);

Now, we can replace the binding with:

1
<button data-bind=”click: addItem, enable: addItemsAllowed>Add Item</button>

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
<button data-bind="click: editItem, visible: !hiddenFlag()">Edit</button>

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
ko.bindingHandlers.hidden = {
  update: function(element, valueAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor());
    ko.bindingHandlers.visible.update(element, function() { return!value; });
  }
};

Now, the binding is simplified to:

1
<button data-bind="click: editItem, hidden: hiddenFlag">Edit</button>

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
<span data-bind=”text: selectedItem() ? selectedItem().name() : unknown’”></span>

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
var viewModel = {
   items: ko.observableArray(),
   selectedItem: ko.observable()
};
1
2
3
4
5
6
7
<ul data-bind="template: { name: 'editorTmpl', data: selectedItem }"></ul>

<script id="editorTmpl" type="text/html">
   <li>
     <input data-bind="value: name" />
   </li>
</script>

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
viewModel.selectedItemName = ko.computed(function() {
  var selected = this.selected();
  return selected ? selected.name() : 'unknown';
}, viewModel);

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
<div data-bind="safeText: { value: selectedItem, property: 'name', default: 'unknown' }"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
ko.bindingHandlers.safeText = {
  update: function(element, valueAccessor, allBindingsAccessor) {
    var options = ko.utils.unwrapObservable(valueAccessor()),
        value = ko.utils.unwrapObservable(options.value),
        property = ko.utils.unwrapObservable(options.property),
        fallback = ko.utils.unwrapObservable(options.default) || "",
        text;

        text = value ? (options.property ? value[property] : value) : fallback;

        ko.bindingHandlers.text.update(element, function() { return text; });
    }
};

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
ko.safeObservable = function(initialValue) {
  var result = ko.observable(initialValue);
  result.safe = ko.dependentObservable(function() {
    return result() || {};
  });

  return result;
};

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
<div data-bind=”text: selectedItem.safe().name></div>

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
<input data-bind="value: name, event: { focus: function() { viewModel.selectItem($data); }, blur: function() { viewModel.selectItem(null); } }" />

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
ko.bindingHandlers.hasFocus = {
    init: function(element, valueAccessor) {
        $(element).focus(function() {
            var value = valueAccessor();
            value(true);
        });
        $(element).blur(function() {
            var value = valueAccessor();
            value(false);
        });
    },
    update: function(element, valueAccessor) {
        var value = valueAccessor();
        if (ko.utils.unwrapObservable(value))
            element.focus();
        else
            element.blur();
    }
};

This would allow you to simplify the binding down to:

1
<input data-bind=”value: name, hasFocus: selectedItem />

Returning a binding object from our view model

1
2
<div data-bind=”css: { success: selectedItem().isSaved, error:
!selectedItem().isSaved() }”>

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
viewModel.selectedSaveClass = ko.dependentObservable(function() {
    var result = {},
        item = this.selectedItem();

    if (item) {
       result.success = item.isSaved();
       result.error = !item.isSaved();
    }

    return result;
}, viewModel);

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
var viewModel = {
  selectedItem: ko.observable()
};

viewModel.selectedItem.saveStyle = ko.dependentObservable(function() {
    var result = {},
        item = this.selectedItem();

    if (item) {
       result.success = item.isSaved();
       result.error = !item.isSaved();
    }

    return result;

}, viewModel);

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
<a href="#" data-bind="click: function() { viewModel.selectItem($data); }">Select</a>

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
ko.bindingHandlers.clickWithData = {
    init: function(element, valueAccessor, allBindingsAccessor, context) {
        var action = valueAccessor(),
        var newValueAccessor = function() {
            return function(event) {
                action.apply(context,[context, event]);
            };
        };

        ko.bindingHandlers.click.init(element, newValueAccessor, allBindingsAccessor, context);
    }
};

If viewModel has global scope, then in your item you can now avoid the anonymous function like:

1
<a href=”#” data-bind=”clickWithData: viewModel.selectItem>Select</a>

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
<ul data-bind="template: { name: 'itemTmpl', foreach: items, templateOptions: { select: selectItem } ]"></ul>

<script id=”itemTmpl” type=”text/html”>

   <li>
      <a href="#" data-bind="clickWithData: $item.selectItem">Select</a>
   </li>
</script>

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
function Item(id, name, selectedItem) {
    return {
        id: ko.observable(id),
        name: ko.observable(name),

        select: function() {
            selectedItem(this);

        }
    };
}

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
<a href="#" data-bind="click: select">Select</a>

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.

Comments