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

Extenders


The last "basic" feature to cover is extenders (don't worry, there is still plenty of advanced stuff to cover). Extenders offer a way to modify individual observables. Two common uses of extenders are as follows:

  • Adding properties or functions to the observable

  • Adding a wrapper around the observable to modify writes or reads

Simple extenders

Adding an extender is as simple as adding a new function to the ko.extenders object with the name you want to use. This function receives the observable being extended (called the target) as the first argument, and any configuration passed to the extender is received as the second argument, as shown in the following code:

ko.extenders.recordChanges = function(target, options) {
  target.previousValues = ko.observableArray();
  target.subscribe(function(oldValue) {
    target.previousValues.push(oldValue);
  }, null, 'beforeChange');
  return target;
};

This extender will create a new previousValues property on the observable. This new property is as an observable array and old values are pushed to it as the original observable is changed (the current value is already in the observable of course).

The reason the extender has to return the target is because the result of the extender is the new observable. The need for this is apparent when looking at how the extender is called:

var amount = ko.observable(0).extend({ recordChanges: true});

The true value sent to recordChanges is received by the extender as the options parameter. This value can be any JavaScript value, including objects and functions.

You can also add multiple extenders to an observable in the same call. The object sent to the extend method will call an observable for every property it contains:

var amount = ko.observable(0).extend({ recordChanges: true,anotherExtender: { intOption: 1});

As the extend method is called on the observable, usually during its initial creation, the result of the extend call is what is actually stored. If the target is not returned, the amount variable would not be the intended observable.

To access the extended value, you would use amount.previousValues() from JavaScript, or amount.previousValues if accessing it from a binding. Note the lack of parentheses after amount; because previousValues is a property of the observable, not a property of the observable's value, it is accessed directly. This might not be immediately obvious, but it should make sense as long as you remember that the observable and the value the observable contains are two different JavaScript objects.

An example of this extender is in the cp1-extend branch.

Extenders with options

The previous example does not pass any options to the recordChanges extender, it just uses true because the property requires a value to be a valid JavaScript. If you want a configuration for your extender, you can pass it as this value, and a complex configuration can be achieved by using another object as the value.

If we wanted to supply a list of values that are not to be recorded, we could modify the extender to use the options as an array:

ko.extenders.recordChanges = function(target, options) {
  target.previousValues = ko.observableArray();
  target.subscribe(function(oldValue) {
    if (!(options.ignore && options.ignore.indexOf(oldValue) !== -1))
      target.previousValues.push(oldValue)
  }, null, 'beforeChange');
  return target;
};

Then we could call the extender with an array:

var history = ko.observable(0).extend({ 
  recordChanges: { ignore: [0, null] } 
});

Now our history observable won't record values for 0 or null.

Extenders that replace the target

Another common use for extenders is to wrap the observable with a computed observable that modifies reads or writes, in which case, it would return the new observable instead of the original target.

Let's take our recordChanges extender a step further and actually block writes that are in our ignore array (never mind that an extender named recordChanges should never do something like this in the real world!):

ko.extenders.recordChanges = function(target, options) {
  var ignore = options.ignore instanceof Array ? options.ignore : [];
  //Make sure this value is available
  var result = ko.computed({
    read: target,
    write: function(newValue) {
      if (ignore.indexOf(newValue) === -1) {
        result.previousValues.push(target());
        target(newValue);
      } else {
        target.notifySubscribers(target());
      }
    }
  }).extend({ notify: 'always'});

  result.previousValues = ko.observableArray();

  //Return the computed observable
  return result;
};

That's a lot of changes, so let's unpack them.

First, to make ignore easier to reference, I've set a new variable that will either be the options.ignore property or an empty array. Defaulting to an empty array lets us skip the null check later, which makes the code a little easier to read. Second, I created a writable computed observable. The read function just routes to the target observable, but the write function will only write to the target if the ignore option doesn't contain the new value. Otherwise, it will notify the target subscribers of the old value. This is necessary because if a UI binding on the observable initiated the change, it needs the illegal change to be reverted. The UI element would already have updated and the easiest way to change it back is through the standard binding notification mechanism that is already listening for changes.

The last change is the notify: always extender that's on the result. This is one of Knockout's default extenders. Normally, an observable will only report changes to subscribers when the value has been modified. To get the observable to reject changes, it needs to be able to notify subscribers of its current unchanged value. The notify extender forces the observable to always report changes, even when they are the same.

Finally, the extender returns the new computed observable instead of the target, so that anyone trying to write a value does so against the computed.

The cp1-extendreplace branch has an example of this binding. Notice that trying to enter values into the input box that are included in the ignored options (0 or an empty string) are immediately reverted.