Binding Plain Javascript Objects

####

Key Points

Configuration

Durandal's observable plugin can hook into the binder and ensure that all bound objects are observable. Let's see how to install the plugin. In your main.js file, before calling app.start() you will need to make a call to configurePlugins(...). Here's what your code might look like:

define(['durandal/app'],  function (app) {
    app.configurePlugins({
        observable: true
    });

    app.start().then(function () {
        app.setRoot('shell');
    });
});

Once you've done that, you have enabled two-way databinding against plain JavaScript objects. You can still use Knockout observables, but they are not necessary.

Usage Scenarios

The observable plugin provides amazing simplifcation, readability and maintainability to your code. But, you must only use it when targeting an ES5 browser with defineProperty support. It will not work without this crucial JavaScript language capability. Supported browsers include:

Once you configure the plugin, all bound objects are observable, even though they have normal properties. There are a couple of interesting scenarios to cover as a result of this change.

Accessing the Underlying Observable

Even though your objects have normal properties, they are actually implemented using ES5 getters and setters. Underlying the property is an actual Knockout observable. Should you need to subscribe to such an observable (even if the plugin has not yet created the getters and setters) you would do it like this:

var observable = require('plugins/observable');

var viewModel:{
    firstName:'',
    lastName:''
};

observable(viewModel, 'firstName').subscribe(function(value){
    console.log('First name changed.');
});

viewModel.firstName = 'Test';

Calling the observable module as a function, and passing the object and the property name, will return the underlying Knockout observable instance. This is also handy for extending observables or passing them around.

Creating Computed Observables

If you wish to create a computed observable, you can use the observable.defineProperty API like so:

var observable = require('plugins/observable');

var viewModel:{
    firstName:'',
    lastName:''
};

observable.defineProperty(viewModel, 'fullName', function(){
    return this.firstName + ' ' + this.lastName;
});

console.log(viewModel.fullName);

The third argument to observable.defineProperty functions just like the argument to a Knockout computed.

Promises

The observable module also understands promises. If you have an attribute that is an instance of a promise on your bound model, the observable module will convert this into a getter that returns the promised value. Under the covers it will register a callback with the promise and then update the observable for you. Here's an example of code you might write in light of this:

define(function (require) {
    var server = require('services/server');

    return {
        activate: function () {
            this.activity = server.getRecentActivity(); //async action returns a promise
            this.news = server.getNews(); //async action returns a promise
        }
    };
});

In your view, you can data-bind to these as if they were normal arrays.

<div data-bind="foreach: news">
    <h1 data-bind="text: title"></h1>
    <p data-bind="text: content"></p>
    <hr>
</div>

Change Detection

The observable module supports optional changed detection. To activate it, simply add the changeDetection parameter to the plugin activation:

  app.configurePlugins({
    observable: {
      changeDetection: "hasChanged" // The name of your method.
    },
    // ... other plugins.
  });

Then, add a method of the same name to your shell, model, viewmodel or controller:

  hasChanged = function (obj, propertyName, newValue, arrayChanges) {
    // obj is the object on which the property "propertyName" has changed.

    if (arrayChanges) {
      // Array changes needs version 3+ of Knockout and is called on changes
      // to an observable array.
    } else {
      // newValue contains the new value of the observable having changed.
    }
  }

Multiple objects can receive notifications when changes are detected. Simply add the method to all the objects you need to detect those changes.