Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Reacting to Changes in KnockoutJS: Choosing the Right Tool for the Right Job

| Comments

When starting out with Knockout, it doesn’t take long before you encounter a situation where observables and the default bindings don’t quite solve your problem in an elegant way. At that point, it really helps to understand the various options available to you in Knockout. Should you create a computed observable? Is it time to write your first custom binding? These tools along with manual subscriptions and writable computed observables give you some powerful ways to react to changes on your view model.

To demonstrate these options, let’s say that we are creating a small application to track store inventory. Here are the requirements of our application:

  1. Track the quantity and retail price of items in the store
  2. Display a total on each line based on the current quantity and price. Visually indicate when the total has been updated after any edits.
  3. Format the price and total as currency.
  4. Default the price of each item based on the selected product’s wholesale price.

Let’s see how we can meet these requirements in Knockout.

computed observables

  • computed observables are great for read-only fields that are based on one or more observables.
  • Our total field fits this requirement. Here is how we would define our computed observable in our Item constructor:
1
2
3
this.total = ko.computed(function() {
    return this.quantity() * this.price();
}, this);
  • Any observables accessed in the function will trigger updates to this field. In this case we are accessing the quantity and price, so our field will be updated any time that the quantity or price of the item are edited.

Custom Bindings

  • Knockout allows you to create custom bindings, if the released options don’t meet your needs.
  • Bindings are actually implemented as computed observables, so any observables accessed in the binding code will create dependencies.
  • The code called by your binding has access to the element containing the binding along with the data passed to it. Additionally, the binding code can access the other bindings listed on the element and the overall object that was the context of the binding.
  • Bindings should be reserved for cases where you need to connect your UI and your view model. While you could write a total binding that updates a field based on the quantity and price, this would make your view model less portable and tie the total concept to this UI.
  • Animations or other functionality that manipulates DOM elements based on changes to the view model are good candidates for bindings.

In our sample, we need to add some animation to the total field whenever it is updated. We can write a wrapper to the text binding that looks like this:

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');
    }
};

Then, we can specify the binding on our total field to cause it to fade in each time that it is modified.

1
<td data-bind="fadeInText: total"></td>

Manual Subscriptions

  • Manual subscriptions can be thought of like bindings for your view model. Bindings allow your UI to react to changes in your view model, while manual subscriptions allow your view model to react to changes to itself. This may mean making updates to other related objects in your view model.
  • A common example is triggering an update to an observable via AJAX when another observable changes, such as when a checkbox is checked or a dropdown value is changed. In this case, the manual subscription acts like an asynchronous computed observable.

In our sample, we can use manual subscriptions to fill in a default value for the price whenever the user selects a different product from the catalog. We would define our subscription in our Item constructor like:

1
2
3
4
5
6
this.productId.subscribe(function(newProductId) {
    var newProduct = ko.utils.arrayFirst(viewModel.productCatalog, function(product) {
        return product.id == newProductId;
    });
    this.price(newProduct.wholesale);
}.bind(this));

Whenever the user selects a different product, we will default the Item’s price to the product’s wholesale price.

Writable computed observables

  • writable computed observables are a powerful feature that were added after the 1.12 release, so they are currently available only in the latest code.
  • They are best used when the value displayed in a field is not the value that you want saved to your model.
  • This functionality can be useful in cases where you want to broker between objects and IDs like when your object stores a reference by ID to an item in another array.
  • A typical example is when you want to format an editable field and then parse any edits to the field, so that you can update the original observable.

In our sample, we can use this concept to display currency for the price and parse the input back into a number to update the price. We would define it like:

1
2
3
4
5
6
7
8
9
10
11
12
//writable computed observable to parse currency input
this.editPrice = ko.computed({
    //return a formatted price
    read: function() {
        return viewModel.formatCurrency(this.price());
    },
    //if the value changes, make sure that we store a number back to price
    write: function(newValue) {
        this.price(viewModel.parseCurrency(newValue));
    },
    owner: this
});

Now, we can bind our input field for price to the editPrice computed observable and we will ensure that our price observable always contains a numeric value.

Completed Sample:

Link to full sample on jsFiddle.net

Comments