Book Image

Mastering KnockoutJS

By : Timothy Moran
Book Image

Mastering KnockoutJS

By: Timothy Moran

Overview of this book

Table of Contents (16 chapters)
Mastering KnockoutJS
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Observables


Knockout follows a publish/subscribe pattern to keep data in sync between different parts of the application, such as the UI and the viewmodel. The publisher in Knockout is the observable object. If you've used MVVM before in Windows Presentation Foundation (WPF) development, then observable objects can be thought of as Knockout's INotifyPropertyChanged implementation.

To construct an observable, the observable function is called on the global ko object:

this.property = ko.observable('default value');

The observable function returns a new observable. If ko.observable is called with a value, it returns an observable with that value.

Note

The reason why Knockout observables are JavaScript functions instead of normal properties is to allow support for older browsers such as Internet Explorer 6, which did not support getters and setters on properties. Without that ability, setting properties would have no mechanism to notify subscribers about changes.

Observables are JavaScript functions that record subscribers reading their value, then call these subscribers when the value has been changed. This is done using Knockout's dependency tracking mechanism.

Observables are read by calling them without any parameters. To write to an observable, call it with the value as the first and only parameter (further parameters are ignored):

var total = vm.total();// read value
vm.total(50);// write new value

Tip

Downloading the sample code

You can download the sample code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

Observables can contain any legal JavaScript value: primitives, arrays, objects, functions, and even other observables (though this wouldn't be that useful). It doesn't matter what the value is; observables merely provide a mechanism to report when that value has been changed.

Observable arrays

Though standard observables can contain arrays, they aren't well suited to track changes in them. This is because the observable is looking for changes in the value of the array, a reference to the array itself, which is not affected by adding or removing elements. As this is what most people expect change notification to look like on an array, Knockout provides the observableArray:

this.users = ko.observableArray(myUsers);

Like observables, arrays can be constructed with an initial value. Normally, you access an observable by calling it or setting its value by passing it a parameter. With observable arrays it's a little different. As the value of the array is its reference, setting that value would change the entire array. Instead, you usually want to operate on the array by adding or removing elements. Consider the following action:

this.users().push(new User("Tim"));

By calling this.users(), the underlying array is retrieved before a new user is pushed to it. In this case, Knockout is not aware that the array was changed, as the change was made to the array itself and not the observable. To allow Knockout to properly track changes, these changes need to be made to the observable, not the underlying value.

To do this, Knockout provides the standard array methods on the observable, namely, push, pop, shift, unshift, sort, reverse, and splice. The call should look like this:

this.users.push(new User("Tim"));

Notice that instead of retrieving the array from the observable, we are calling push directly on the observable. This will ensure that subscribers are notified of the change with an updated array.

Computed observables

Observables are properties that are set manually, either through your code or by bindings from the UI. Computed observables are properties that automatically update their value by responding to changes in their dependencies, as shown in the following code:

var subtotal = ko.observable(0);
var tax = ko.observable(0.05);
var total  = ko.computed(function() {
  return parseFloat(subtotal()) * (1 + parseFloat(tax()));
});

In this example, subtotal and tax are the dependencies of the total computed observable. For the first time, the computed observable calculates records of any other observables that were accessed and creates a subscription for them. The result is that whenever subtotal or tax are changed, the total is recalculated and notified to its subscribers. It helps to think of computed observables as declarative values; you define their value as a formula and they will keep themselves up to date.

The parseFloat calls are to ensure that they are treated as numbers instead of strings, which would cause concatenation instead of arithmetic. As Knockout binds data against HTML attributes, which are always strings, updates from data binding produce strings. When we discuss extenders, you will see another way to manage this issue.

You can see a sample of this on the cp1-computeds branch:

Try changing some of the numbers and watch the total computed value update automatically. You can see that the viewmodel code contains just this sample by looking in the client/app/sample.js file.

Writable computed observables

The preceding total example is a read-only computed. While they are less common, it is also possible to make a computed observable writable. To do so, pass an object with a read and write function to ko.computed:

var subtotal = ko.observable(0);
var tax = ko.observable(0.05);
var total  = ko.computed({
  write: function(newValue) {
      subtotal(newValue / (1 + parseFloat(self.tax())));
  },
  read: function() {
      parseFloat(subtotal()) * (1 + parseFloat(tax()));
 }
});

When something attempts to write to the total computed now, it will cause the subtotal observable to be updated by the write function. This is a very powerful technique, but it is not always necessary. In some cases being unable to write directly to total might be a good thing, such as when total might involve conditionally applying tax to a list of items. You should use writeable computeds only when it makes sense to do so.

You can see an example of this in the cp1-writecomputed branch. The total computed is now bound to an input element such as the subtotal and tax properties, and changes to the value will reflect back into the subtotal observable.

Pure computed observables

Nonpure computed observables re-evaluate themselves whenever any of their dependencies change, even if there are no subscribers to receive the updated value. This re-evaluation can be useful if the computed also has intentional side effects, but it wastes memory and the processor's cycles if it has no side effects. Pure computed observables, on the other hand, do not re-evaluate when there are no subscribers.

Pure computed observables have two states: listening and sleeping. When a pure computed has subscribers, it will be listening and behaving exactly like a normal computed. When a pure computed has no subscribers, it will enter its sleeping state and dispose off all of its dependency subscriptions. When it wakes up, the pure computed will re-evaluate itself to ensure its value is correct.

Pure computed observables are useful when a value may go unused for an extended period of time, as they do not re-evaluate. However, since a pure computed always re-evaluates when accessed from a sleeping state, it can sometimes perform worse than a normal computed observable. Since normal computeds only re-evaluate when their dependencies change, a computed observable that is frequently woken from a sleeping state could potentially evaluate its dependencies more often.

There are two ways to create a pure computed: by using ko.pureComputed or by passing { pure: true } as the third parameter to ko.computed:

var total = ko.pureComputed(function() {
  return parseFloat(subtotal()) * (1 + parseFloat(tax()));
});
//OR
var total = ko.computed(function() {
  return parseFloat(subtotal()) * (1 + parseFloat(tax()));
}, this, { pure: true });

Note

Pure computed observables were introduced in Knockout 3.2, which was not released at the time this book was written. None of the code samples take advantage of pure computed observables, even though many of the samples would have benefited from them.

Manual subscriptions

Sometimes you need to do more than update a dependent value when an observable changes, such as make a web request for additional data based on the new value of your observable. Observables provide a subscribe function that lets you register a function to be called when the observable is updated.

Subscriptions use the same internal mechanism in Knockout that binding handlers and computed observables use to receive changes.

This is an example of setting up a subscription on an observable:

var locationId = ko.observable();
locationId.subscribe(function (newLocationId) {
  webService.getLocationDetails(newLocationId);
});

This subscription will be called any time when the locationId is updated, whether it happens from a UI binding or from somewhere else in JavaScript.

The subscribe function also allows you to provide a target for the subscription and the name of the event you want to subscribe to. The target is the value of this for the subscription handler you provide. The event defaults to change, which receives the value after it has been updated, but can also be beforeChange, which is called with the old value before a change happens:

locationId.subscribe(function (oldValue) {
  console.log("the location " + oldValue + " is about to change");
}, self, 'beforeChange');});

Finally, you can stop a subscription from continuing to fire by capturing it and calling dispose. This can be useful if you want to stop the handler or to make subscriptions that only fire a single time:

var subscription = locationId.subscribe(function (newValue) {
  console.log("the location " + oldValue + " is about to change");
  subscription.dispose();
});

Once a subscription has been disposed, it cannot be restarted. If you need it, you will have to recreate the subscription.

The cp1-subscribe branch has a subscription example that logs any changes to the subtotal observable on the JavaScript console, as well as a button that stops the subscription. Try changing the subtotal or total value and watch out for the console messages. Changing the total causes an update of the subtotal, which is why it still fires the subscription. Remember, changes from any source will cause an observable to report changes to all of its subscribers. This is the same reason updating the total computed causes the subtotal observable's input element to update; the input element is a subscriber to the viewmodel's property.