Aurelia property observation strategies

Aurelia employs several strategies when observing object properties. The best strategy is chosen depending on the features of the browser and the type of object/property being observed. Here's a run-down of the techniques used, in order of priority.

Note: the following doesn't cover Aurelia's strategies for observing Arrays. That's a subject for another post :)

1. Is the object being observed a DOM element?

In this case an observer is chosen based on the element's type and the name of the element attribute being bound:

  • <select> element's value attribute? Use the SelectValueObserver which simplifies common scenarios like two-way binding strings or objects or binding arrays in multi-select scenarios.

  • <input> element's checked attribute? Use the CheckedObserver which enables two-way binding a group of radios to a single "selected item" property as well as checkboxes to a "selected items" array. Here are a few examples of what's possible with checked binding.

  • <input> or <textarea> element's value attribute? Use the ValueAttributeObserver which enables two way binding of input values.

  • xlink:, data-*, aria-* attribute? Each of these require specific logic around attribute value assignment and retrieval. Aurelia includes specialized observers for these cases.

  • style attribute or it's alias, the css attribute? Use the StyleObserver which enables binding strings or objects to an element's style.cssText.

  • All other scenarios fall through to #2 below...

2. Does the property being observed have a getter function (ie was the property defined using Object.defineProperty)?

Object.observe cannot be used in this scenario, at least not until Object.getNotifier is well supported across browsers. In this case Aurelia will first check whether dependencies have been declared for the property. If no dependencies are declared Aurelia will check whether a property observation adapter knows how to observe the property. Otherwise Aurelia falls back to dirty checking.

3. OK, we have a standard object property...

Use the preferred observation strategy, Object.observe if the browser supports it, otherwise re-write the property using Object.defineProperty so Aurelia can intercept the property assignments.

All this strategy-picking logic is encapsulated in aurelia/binding's ObserverLocator class.

Declaring Property Dependencies

Any time you create a computed property on your view model you're introducing a situation where Aurelia needs to use dirty-checking to observe the property. Most of the time this isn't a big deal but in situations where dirty-checking is used a lot your app will use more memory and potentially perform poorly.

The Aurelia "starter kit" app: skeleton-navigation, includes a scenario where dirty-checking is required: the fullName property.

export class Welcome{
  constructor(){
    this.heading = 'Welcome to the Aurelia Navigation App!';
    this.firstName = 'John';
    this.lastName = 'Doe';
  }

  // **this property will require dirty-checking**
  get fullName(){
    return `${this.firstName} ${this.lastName}`;
  }

  welcome(){
    alert(`Welcome, ${this.fullName}!`);
  }
}

To avoid dirty-checking we can tell Aurelia the fullName property's dependencies are firstName and lastName. This will save Aurelia from having to constantly poll the fullName property for changes because it can observe firstName and lastName directly.

Here's how you declare property dependencies today:

// add this line:
import {declarePropertyDependencies} from 'aurelia-framework';

// nothing changes in the class itself.
export class Welcome{
  ...
}

// and add this line:
declarePropertyDependencies(Welcome, 'fullName', ['firstName', 'lastName']);  

Decorators are now supported in babel and Typescript 1.5 alpha. With this important feature in place declaring property dependencies in Aurelia will be even easier:

@computedFrom('firstName', 'lastName')
get fullName(){
  return `${this.firstName} ${this.lastName}`;
}

Property Observation Adapters

The Aurelia binding system is pluggable. This means you can supply an adapter to Aurelia that can observe certain types of properties when Aurelia isn't able to use a preferred binding strategy.

A couple binding adapters are already in development. One is for observing Breeze entities. Breeze is a data management framework that employs property getters and setters to enable entity-state tracking when properties are changed. You can find the aurelia-breeze plugin here.

A second property observation adapter for KnockoutJS observables is in development.

Using the ObserverLocator

Situations often crop up where you need to observe a property's changes, outside of the standard data-binding scenarios Aurelia makes easy. It would be nice if we could simply use Object.observe in these scenarios. Unfortunately caniuse says this is not an option in today's browser environment.

The good news is you can use Aurelia's ObserverLocator which provides a cross-browser approach for observing properties. All you need to do is grab the observer locator from the container and start subscribing.

import {ObserverLocator} from 'aurelia-binding';  // or 'aurelia-framework'

class Foo {
  static inject function() { return [ObserverLocator]; }
  constructor(observerLocator) {
    // the property we'll observe:
    this.bar = 'baz';
    
    // subscribe to the "bar" property's changes:
    var subscription = this.observerLocator
      .getObserver(this, 'bar')
      .subscribe(this.onChange);
  }
  
  onChange(newValue, oldValue) {
    alert(`bar changed from ${oldValue} to ${newValue}`);
  }
}

As with any event subscription you need to unsubscribe/dispose-it when you no longer need it, to prevent memory leaks, etc. This is why the subscribe method returns a function you can invoke to dispose of the subscription.

Note: future versions of Aurelia will include a more streamlined way to subscribe to property changes.

Doing more with the ObserverLocator

A scenario came up in the Aurelia gitter chat the other day where someone needed to update a property in their view-model when properties in another object changed. To simplify the logic around this we put together a generic "MultiObserver" class that can observe any number of object properties and invoke a callback when something changes:

import {ObserverLocator} from 'aurelia-framework'; // or 'aurelia-binding'

export class MultiObserver {
  static inject() { return [ObserverLocator]; }
  constructor(observerLocator) {
    this.observerLocator = observerLocator;
  }

  observe(properties, callback) {
    var subscriptions = [], i = properties.length, object, propertyName;
    while(i--) {
      object = properties[i][0];
      propertyName = properties[i][1];
      subscriptions.push(this.observerLocator.getObserver(object, propertyName).subscribe(callback));
    }

    // return dispose function
    return () => {
      while(subscriptions.length) {
        subscriptions.pop()();
      }
    }
  }
}

Using the MultiObserver we can create an OilSpeculator class that reacts to the different factors involved in speculating oil prices:

import {MultiObserver} from 'multi-observer';
import {OilReserves} from 'oil-reserves';
import {Mood} from 'temperment';
import {FudgeFactor} from 'guessing';

export class OilSpeculator {
  static inject() { return [OilReserves, Mood, FudgeFactor, MultiObserver]; }
  constructor(oilReserves, mood, fudgeFactor, multiObserver) {
    this.oilReserves = oilReserves;
    this.mood = mood;
    this.fudgeFactor = fudgeFactor;
    this.speculateOilPrice();
    
    // speculate the oil price when something changes... 
    multiObserver.observe(
      [[oilReserves, 'barrels'],
       [mood, 'crankiness'],
       [fudgeFactor, 'value']],
      () => this.speculateOilPrice());
  }
  
  speculateOilPrice() {
    this.oilPrice = this.mood.crankiness * this.oilReserves.barrels / this.fudgeFactor.value;
  }
}