Advertisement

Knockout Observables

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

This post is part of a series called Knockout Succinctly.
Hello, Knockout
Control-Flow Binding

We’ve seen how observable properties let Knockout.js automatically update HTML elements when underlying data changes, but this is only the beginning of their utility. Knockout.js also comes with two more ways of exposing ViewModel properties: computed observables and observable arrays. Together, these open up a whole new world of possibilities for data-driven user interfaces.

Computed observables let you create properties that are dynamically generated. This means you can combine several normal observables into a single property, and Knockout.js will still keep the view up-to-date whenever any of the underlying values change.

Figure 12: A computed observable dependent on two normal observables

Observable arrays combine the power of Knockout.js’ observables with native JavaScript arrays. Like native arrays, they contain lists of items that you can manipulate. But since they’re observable, Knockout.js automatically updates any associated HTML elements whenever items are added or removed.

Figure 13: An observable array containing other ViewModels

The ability to combine observables, along with the ability to work with lists of items, provides all the data structures you’ll need in a ViewModel. This lesson introduces both topics with a simple shopping cart interface.


Computed Observables

First, we’ll start with a simple computed observable. Underneath the firstName and lastName observables in PersonViewModel, create the fullName computed observable:

this.fullName = ko.computed(function() {
  return this.firstName() + " " + this.lastName();
}, this);

This defines an anonymous function that returns the person’s full name whenever PersonViewModel.fullName is accessed. Dynamically generating the full name from the existing components (firstName and lastName) prevents us from storing redundant data, but that’s only half the battle. We need to pass this function to ko.computed() to create a computed observable. This tells Knockout.js that it needs to update any HTML elements bound to the fullName property whenever either firstName or lastName change.

Let’s make sure our computed observable works by binding the “John’s Shopping Cart” line to fullName instead of firstName:

<p><span data-bind='text: fullName'></span>'s Shopping Cart</p>

Now your page should read “John Smith’s Shopping Cart.” Next, let’s make sure that Knockout.js keeps this HTML element in sync when we change one of the underlying properties. After binding an instance of PersonViewModel, try changing its firstName property:

var vm = new PersonViewModel();
ko.applyBindings(vm);
vm.firstName("Mary");

This should change the line to “Mary Smith’s Shopping Cart.” Again, remember that reading or setting observables should be done with function calls, not the assignment (=) operator.

Computed observables provide many of the same benefits as Knockout.js’ automatic synchronization of the view. Instead of having to keep track of which properties rely on other parts of the ViewModel, computed observables let you build your application around atomic properties and delegate dependency tracking to Knockout.js.


Observable Arrays

Observable arrays let Knockout.js track lists of items. We’ll explore this by creating a shopping cart display page for our user. First, we need to create a custom object for representing products. At the top of our script, before defining PersonViewModel, add the following object definition:

function Product(name, price) {
  this.name = ko.observable(name);
  this.price = ko.observable(price);
}

This is just a simple data object to store a few properties. Note that it’s possible to give multiple objects observable properties, and Knockout.js will manage all of the interdependencies on its own. In other words, it’s possible to create relationships between multiple ViewModels in a single application.

Next, we’re going to create a few instances of our new Product class and add them to the user’s virtual shopping cart. Inside of PersonViewModel, define a new observable property called shoppingCart:

this.shoppingCart = ko.observableArray([
  new Product("Beer", 10.99),
  new Product("Brats", 7.99),
  new Product("Buns", 1.49)
]);

This is a native JavaScript array containing three products wrapped in an observable array so Knockout.js can track when items are added and removed. But, before we start manipulating the objects, let’s update our view so we can see the contents of the shoppingCart property. Underneath the <p> tag, add the following:

<table>
    <thead>
      <tr>
        <th>Product</th>
        <th>Price</th>
      </tr>
    </thead>
    <tbody data-bind='foreach: shoppingCart'>
      <tr>
        <td data-bind='text: name'></td>
        <td data-bind='text: price'></td>
      </tr>
    </tbody>
  </table>

This is a typical HTML 5 table containing a column for product names and another for product prices. This example also introduces a new binding called foreach. When Knockout.js encounters foreach: shoppingCart, it loops through each item in the ViewModel’s shoppingCart property. Any markup inside of the loop is evaluated in the context of each item, so text: name actually refers to shoppingCart[i].name. The result is a table of items alongside their prices:

Figure 14: Screenshot of the rendered product listing

The details of the foreach binding are outside the scope of this lesson. The next lesson provides an in-depth discussion of foreach, and it also introduces Knockout.js’ other control-flow bindings. For now, let’s get back to observable arrays.

Adding Items

The whole point of using observable arrays is to let Knockout.js synchronize the view whenever we add or remove items. For example, we can define a method on our ViewModel that adds a new item, like so:

this.addProduct = function() {
   this.shoppingCart.push(new Product("More Beer", 10.99));
};

Then, we can create a button to call the method so we can add items at run time and see Knockout.js keep the list up-to-date. Next to the checkout button in the view code, add the following:

<button data-bind='click: addProduct'>Add Beer</button>

When you click this button, the ViewModel’s addProduct() method is executed. And, since shoppingCart is an observable array, Knockout.js inserts another <tr> element to display the new item. Letting Knockout.js keep track of list items like this is much less error-prone than trying to manually update the <table> whenever we change the underlying array.

It’s also worth pointing out that Knockout.js always makes the minimal amount of changes necessary to synchronize the user interface. Instead of regenerating the entire list every time an item is added or removed, Knockout.js tracks which parts of the DOM are affected and updates only those elements. This built-in optimization makes it possible to scale up your application to hundreds or even thousands of items without sacrificing responsiveness.

Deleting Items

Similarly, Knockout.js can also delete items from an observable array via the remove() method. Inside of the PersonViewModel definition, add another method for removing items:

this.removeProduct = function(product) {
  this.shoppingCart.remove(product);
};

Then, add a delete button for each item in the <tbody> loop:

<tr>
  <td data-bind='text: name'></td>
  <td data-bind='text: price'></td>
  <td><button data-bind='click: $root.removeProduct'>Remove</button></td>
</tr>

Because we’re in the foreach context, we had to use the $root reference to access our ViewModel instead of the current item in the loop. If we tried to call removeProduct() without this reference, Knockout.js would have attempted to call the method on the Product class, which doesn’t exist. All of the available binding contexts for foreach are covered in the next lesson.

The fact that we’re in a foreach loop also messes up the this reference in removeProduct(), so clicking a Remove button will actually throw a TypeError. We can use a common JavaScript trick to resolve these kinds of scope issues. At the top of the PersonViewModel definition, assign this to a new variable called self:

function PersonViewModel() {
  var self = this;
  ...

Then, use self instead of this in the removeProduct() method:

this.removeProduct = function(product) {
  self.shoppingCart.remove(product);
};

You should now be able to manipulate our observable array with the Add Beer and Remove buttons. Also note that Knockout.js automatically adds the current item in the loop as the first parameter to removeProduct().

Destroying Items

The remove() method is useful for real-time manipulation of lists, but it can prove troublesome once you start trying to send data from the ViewModel to a server-side script.

For example, consider the task of saving the shopping cart to a database every time the user added or deleted an item. With remove(), the item is removed immediately, so all you can do is send your server the new list in its entirety—it’s impossible to determine which items where added or removed. You either have to save the entire list, or manually figure out the difference between the previous version stored in the database and the new one passed in from the AJAX request.

Neither of these options is particularly efficient, especially considering Knockout.js knows precisely which items were removed. To remedy this situation, observable arrays include a destroy() method. Try changing PersonViewModel.removeProduct() to the following:

this.removeProduct = function(product) {
  self.shoppingCart.destroy(product);
  alert(self.shoppingCart().length);
};

Now when you click the Remove button, Knockout.js won’t remove the item from the underlying array. This is shown in the alert message, which should not decrease when you click “Remove.” Instead of altering the list, the destroy() method adds a _destroy property to the product and sets it to true. You can display this property by adding another alert message:

alert(product._destroy);

The _destroy property makes it possible to sort through an observable list and pull out only items that have been deleted. Then, you can send only those items to a server-side script to be deleted. This is a much more efficient way to manage lists when working with AJAX requests.

Note that the foreach loop is aware of this convention, and still removes the associated <tr> element from the view, even though the item remains in the underlying array.

Other Array Methods

Internally, observable arrays are just like normal observable properties, except they are backed by a native JavaScript array instead of a string, number, or object. Like normal observables, you can access the underlying value by calling the observable array without any properties:

this.debugItems = function() {
  var message = "";
  var nativeArray = this.shoppingCart();
  for (var i=0; i<nativeArray.length; i++) {
    message += nativeArray[i].name + "\n";
  }
  alert(message);
};

Calling this method will loop through the native list’s items, and it also provides access to the native JavaScript array methods like push(), pop(), shift(), sort(), etc.

However, Knockout.js defines its own versions of these methods on the observable array object. For example, earlier in this lesson, we used shoppingCart.push() to add an item instead of shoppingCart().push(). The former calls Knockout.js’ version, and the latter calls push() on the native JavaScript array.

It’s usually a much better idea to use Knockout.js’ array methods instead of accessing the underlying array directly because it allows Knockout.js to automatically update any dependent view components. The complete list of observable array methods provided by Knockout.js follows. Most of these act exactly like their native JavaScript counterparts.

  • push()
  • pop()
  • unshift()
  • shift()
  • slice()
  • remove()
  • removeAll()
  • destroy()
  • destroyAll()
  • sort()
  • reversed()
  • indexOf()

Summary

In this lesson, we saw how computed observables can be used to combine normal observables into compound properties that Knockout.js can track. We also worked with observable arrays, which are a way for Knockout.js to synchronize lists of data in the ViewModel with HTML components.

Together, atomic, computed, and array observables provide all the underlying data types you’ll ever need for a typical user interface. Computed observables and observable arrays make Knockout.js a great option for rapid prototyping. They let you put all of your complex functionality one place, and then let Knockout.js take care of the rest.

For example, it would be trivial to create a computed observable that calculates the total price of each item in the shoppingCart list and displays it at the bottom of the page. Once you create that functionality, you can reuse it anywhere you need the total price (e.g., an AJAX request) just by accessing a ViewModel property.

The next lesson introduces control-flow bindings. The foreach binding that we used in this lesson is probably the most common control-flow tool, but Knockout.js also includes a few more bindings for fine-grained control over our HTML view components.

This lesson represents a chapter from Knockout Succinctly, a free eBook from the team at Syncfusion.

Advertisement