Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

10 Things to Know About KnockoutJS on Day One

| Comments

I have been working with Knockout.js on a daily basis now for about six months. Much of that time has been spent trying to help folks on the KO forums and StackOverflow troubleshoot problems and brainstorm ideas. Here is a list of some areas that I feel are commonly misunderstood or overlooked by people that are starting out with Knockout.

1 - How to set and read observables

Observables are functions. The actual value and subscribers to the observable are cached internally by the function. You set an observable’s value by passing the new value as the only argument to the function and you read the value by passing no arguments.

1
2
3
var name = ko.observable("Bob"); //initialize with a value
name("Ted"); //set it to a new value
alert(name()); //read the value

Question: If you need to reference it as a function to read the value, then how come in a data-bind attribute you typically specify just the property name?

Answer: Most bindings will call ko.utils.unwrapObservable on the value passed to it, which will safely return the value for both observables and non-observables. However, in a binding if you use an observable in an expression, then you need to reference it as a function. Likewise, in code you typically need to reference your observables as functions, unless you actually want to pass the observable itself (not the value).

1
2
3
<div data-bind="visible: someFlag">...</div>

<div data-bind="visible: !someFlag()">...</div>

2 – Templates are flexible

The template binding is quite flexible. Here are a few things that you will likely want to do with it before long:

The template binding accepts a data argument that allows you to control the context of the binding. This is handy for simplifying references to nested content. It also accepts an if parameter that helps to handle when the observable value may be null.

1
<div data-bind="template: { name: 'nestedTmpl', 'if': myNestedObject, data: myNestedObject }"></div>

The template binding also accepts a foreach parameter to loop through items in the array passed to it. If the array is observable and changes, then Knockout efficiently adds or removes DOM nodes appropriately rather than re-rendering the entire template (as it does when using jQuery Templates syntax).

1
<ul data-bind="template: { name: 'itemTmpl', foreach: items }"></ul>

The template binding accepts a templateOptions parameter that lets you pass additional data in to your template. This could include methods or observables from your view model. This is crucial for cases where scope would prevent you from accessing the proper variables from inside your template. More info in this post.

1
<ul data-bind="template: { name: 'itemTmpl', foreach: items, templateOptions: { selected: selectedItem } }"></ul>

update: 5/1/2012 - templateOptions is only supported when using jQuery Templates and is not used in KO native templates that were introduced in KO 2.0

Another useful feature to consider is that the name of the template that is applied can be dynamic. More details in this post.

3 – The basic rules of computed observables

By default, the value of a computed observable is determined at the time of creation. However, this behavior can be controlled by creating the computed observable using object syntax and passing in deferEvaluation: true.

1
2
3
4
5
6
7
8
9
viewModel.total = ko.computed({
    read: function() {
       var result = 0;
        ko.utils.arrayForEach(viewModel.items(), function(item) {
            result += item.amount();
        });
    },
    deferEvaluation: true  //don't evaluate until someone requests the value
}, viewModel);

A computed observable will be re-evaluated whenever one of the observables that it accessed in its last evaluation changes. Dependency detection is done each time that the computed observable is evaluated. In the snippet below, if enabled is true, then it will not depend on disabledHelp. However, if enabled becomes false, then it will no longer depend on enabledHelp and will start depending on disabledHelp.

1
2
3
4
//this computed observable will always depend on this.enabled and will additionally depend on either this.enabledHelp or this.disabledHelp.
viewModel.helpText = ko.computed({
    return this.enabled() ? this.enabledHelp() : this.disabledHelp();
}, viewModel);

4 - Manual subscriptions are quite useful

Manual subscriptions give you a chance to programmatically react to an observable changing. This is great for setting defaults and triggering AJAX requests. You are able to manually subscribe to observables, observableArrays, and computed observables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//trigger an AJAX request to get details when the selection changes
viewModel.selectedItem.subscribe(function(newValue) {
    $.ajax({
        url: '/getDetails',
        data: ko.toJSON({
            id: newValue.id
        }),
        datatype: "json",
        contentType: "application/json charset=utf-8",
        success: function(data) {
            viewModel.details(data.details);
        }
    });
});

5 - “this” is really important.

Understanding the value of this when your functions are called is even more important in Knockout than normal, because you are referencing functions in bindings that are being executed indirectly.

Suppose, our view model has a method that alerts on a name property:

1
2
3
viewModel.whoAmI = function() {
   alert(this.name);
};

If I use this method on a click binding within a template that is looping through an array of items, then this will actually be my item and not the viewModel. This is because viewModel.whoAmI just points to the function and Knockout executes it under the context of the data being bound.

1
2
3
4
5
6
<ul data-bind="template: { name: 'itemsTmpl', foreach: items }"></ul>
<script id="itemsTmpl" type="text/html">
     <li>
         <a href="#" data-bind="click: viewModel.whoAmI">Who am I?</a>
     </li>
</script>

Knockout does provide an implementation of bind that you can use on any function, when you want to guarantee the context that it will run under. In this case, it would look like:

1
2
3
viewModel.whoAmI = function() {
   alert(this.name);
}.bind(viewModel);

Manual subscriptions and computed observables do take in a second argument to control the value of this when they are executed.

1
2
3
4
5
6
7
8
9
viewModel.fullName = ko.dependentObservable(function() {
    return this.firstName() + " " + this.lastName();
}, viewModel);

viewModel.gratuityAdded.subscribe(function(newValue) {
    if (newValue) {
       this.total(this.total() * 1.15);
    }
}, viewModel);

6 - KO utility functions (ko.utils)

Get to know the Knockout utility functions, as you will probably start to use them throughout your Knockout code. They are quite useful for navigating and manipulating your view model, although you might already be using similar functions from other libraries. This post describes some of the more useful ones.

Also, take a look at their source code, as their implementations are fairly straightforward.

7 - An observableArray is just an extended observable

An observableArray is actually just an observable. They follow the same rules and have the same features as observables.

An observableArray also has some extra methods added to it to perform basic array operations. These functions perform their action on the underlying array and then notify subscribers that there was a change. These methods include pop, push, reverse, shift, sort, splice, and unshift.

In addition to the those operations, there are several other methods added for common operations. These include remove, removeAll, destroy, destroyAll, replace, and indexOf (which I always forget).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//remove an item
items.remove(someItem);

//remove all items with the name "Bob"
items.remove(function(item) {
    return item.name === "Bob"
});
//remove all items
items.removeAll();

//pass in an array of items to remove
items.removeAll(itemsToRemove)

//retrieve the index of an item
items.indexOf(someItem);

//replace an item
item.replace(someItem, replaceItem);

Note: destroy/destroyAll work like remove/removeAll, except they only mark the items as destroyed and don’t actually remove them.

8 - Custom bindings need not be a last resort

There seems to be a misconception that custom bindings should only be considered if there is no other way to accomplish the desired functionality with the default bindings. I actually think that custom bindings can be used in a variety of situations and should be considered one of the normal tools that you use along with dependentObservables and manual subscriptions. Besides helping control custom behavior and/or interacting with 3rd party components, they can also be used to simplify your bindings by encapsulating multiple behaviors.

The most basic custom binding is usually a wrapper to an existing binding.

1
2
3
4
5
6
7
ko.bindingHandlers.fadeInText = {
    update: function(element, valueAccessor) {
        $(element).hide();
        ko.bindingHandlers.text.update(element, valueAccessor);
        $(element).fadeIn('slow');
    }
};

Anytime that you find your JavaScript code starting to deal with DOM elements, you will likely want to consider placing it into a binding. Given the element, your data, and the values passed to the binding, you can really take control of anything with Knockout. It is also useful to take a look at the existing bindings in the source code, as they are not too hard to digest and provide patterns that can be used in custom bindings.

9 - ko.toJSON has multiple uses

ko.toJSON can be used to convert objects that include observables to a JSON string that is ready to send to the server.

Also, it can be really useful for debugging. Put a div at the bottom of your page and display some or all of your viewModel using ko.toJSON to get a live preview of how changes in your UI are affecting the underlying data. No need for console.logs or alerts.

1
2
3
<hr />
<h2>Debug</h2>
<div data-bind="text: ko.toJSON(viewModel)"></div>

10 – The KO forums are a great place to ask for help, look for solutions, and to share ideas

Stop by the Knockout forums and share your problems, ideas, and thoughts. It is always interesting to hear new perspectives and new ways that people want to use Knockout. You will generally get civil, helpful, and timely responses to your posts.

Comments