How To Observe Objects In Aurelia Using The Binding Engine

When it comes to objects, Aurelia couldn’t make it any easier to observe them for changes and react accordingly and in this topic, we’ll be showing you how easy it is to watch object properties for changes in your Aurelia applications.

Say hello to BindingEngine

Aurelia’s binding system provides an interface called BindingEngine which exposes a few methods for watching not only objects, but collections like arrays and maps. We’ll talk about other collections in a separate topic.

The BindingEngine has a method called propertyObserver which accepts two arguments. The first argument is the object you want to observe and the second argument is the name of the property (as a string).

If you are familiar with the Event Aggregator, then the syntax for observing object properties will look very similar in the form of subscribing to the property change via the subscribe method.

The subscribe method works very similarly to how @observable and @bindable works (if you’re familiar with those). When a value changes, a change callback is fired with the new value as the first argument and old (previous) value as the second. If there is no previous value, it’ll be undefined.

A basic example

import {BindingEngine} from 'aurelia-framework';

class MyViewModel {
    static inject = [BindingEngine];

    constructor(bindingEngine) {
        this.bindingEngine = bindingEngine;
        this.observeMe = 'myvalue';
    }

    attached() {
        this.subscription = this.bindingEngine
            .propertyObserver(this, 'observeMe')
            .subscribe((newValue, oldValue) => { 
                this.objectValueChanged(newValue, oldValue) 
            });
    }

    detached() {
        this.subscription.dispose();
    }

    objectValueChanged(newValue, oldValue) {
        console.log(`observeMe value changed from: ${oldValue} to:${newValue}`);
    }
}

In our above example we are injecting the BindingEngine instance into our view-model, storing it on the class so we can use it and then setting up a subscription to an object property.

The best place to setup any kind of observer or event is within attached so we can store the reference and then inside of detached when the view-model gets teared down, we dispose of it to free up the memory and let the natural browser garbage collection do its thang.

You might notice we are not observing an object, we are actually observing the current view-model and a specific property on it. We won’t get into specifics on classes, but all you need to know is functions/classes with properties work the same way as a plain old object.

In our subscribe handler, we are using an arrow function (to retain scope to the current view-model) and then manually firing a change callback ourselves. If you were to pass the callback directly, you’d lose the current scope.

In this example, our change callback will only fire once (when the value is initialized). We won’t have a previous value either, so the second argument on our change callback will be undefined. So, now we’ll make it so the value changes a couple of times and see what values our change callback is given.

Triggered changes

Taking the code we wrote earlier, let’s add in a setInterval which changes a property value every 2 seconds. This means you should see the change callback firing once every two seconds.

import {BindingEngine} from 'aurelia-framework';

class MyViewModel {
    static inject = [BindingEngine];

    constructor(bindingEngine) {
        this.bindingEngine = bindingEngine;
        this.observeMe = 'myvalue';
    }

    attached() {
        this.subscription = this.bindingEngine
            .propertyObserver(this, 'observeMe')
            .subscribe((newValue, oldValue) => { 
                this.objectValueChanged(newValue, oldValue) 
            });
         
        setInterval(() => {
            this.observeMe = 'Cool date ' + new Date();
        }, 2000);
    }

    detached() {
        this.subscription.dispose();
    }

    objectValueChanged(newValue, oldValue) {
        console.log(`observeMe value changed from: ${oldValue} to:${newValue}`);
    }
}

If you open up your browser developer tool console, you should see a change console.log with the previous and current value being displayed. Because we used setTimeout you’ll see it continually spam the console.

Caveats

The propertyObserver doesn’t really have many caveats, the only thing you need to be aware of is you can only watch a single property. You can create multiple propertyObserver calls to observe multiple properties, but you cannot observe objects as a whole (as there are no native events fired when they change).

Conclusion

The propertyObserver method is simple and powerful. Use it to watch object properties on classes and even injected singletons from elsewhere in your application to reactively perform actions based on property changes.

9 Likes

One note is that this can also be achieved if you are using ES2017+ decorators we have two decorators which can typically be used -

import {observable, bindable} from 'aurelia-binding'; // or aurelia-framework if you have no reference to binding in your app yet

export class MyViewModel {
  @observable name = 'Jonathan Snow';
  @observable age = 32;

  nameChanged(newValue, oldValue) { 
    console.log('New name - ', newValue); 
  }
  ageChanged(newValue, oldValue) { 
    console.log('New age - ', newValue); 
  }
}

But not for observing changes to arrays.

4 Likes

Can you also please give an example of @bindable?

Any best practices for intercepting and modifying the observed value before it’s set?

@bindable works with the “Changed()” naming convention for observables. So…

import { bindable } from 'aurelia-binding'; // or aurelia-framework if you have no reference to binding in your app yet

export class MyViewModel {
  @bindable name = 'Jonathan Snow';
  @bindable age = 32;

  nameChanged(newValue, oldValue) { 
    console.log('New name - ', newValue); 
  }
  ageChanged(newValue, oldValue) { 
    console.log('New age - ', newValue); 
  }
}

3 Likes

https://github.com/aurelia/binding/pull/623

https://github.com/aurelia/templating/pull/558

The aboce PR wills give ability to intercept the incoming value. The bindable PR also has documentation changes. But atm, you can look at its usage from observable PR

They are quite ready for review. You can help pushing it forward by testing and finding edge cases

5 Likes

Thanks, but can I bother you for a clarification?

What is the difference between @observable name vs @bindable name?
Looking from the examples, they both seem to trigger nameChanged(newValue, oldValue).

Example usage of @bindable would be for objects that you are binding in your views and for observing. It “wires up” your object (e.g. makes the attr “my-object” available for HTML, etc.) so that it can be bound in your views and is observed. @observable is mainly for just observing.

ViewModel (component)

import { bindable } from 'aurelia-framework';

export class Foo {
    @bindable myObject; //note you can also customize your object such as binding method (one/two way, etc.)
}

View (parent using aforementioned component)

<template>
    <require from="./foo"></require>

    <foo my-object.bind="myOtherObject"></foo>
</template>

See http://aurelia.io/docs/templating/custom-elements#bindable-properties for more info on @bindable decorator and http://aurelia.io/docs/binding/binding-observable-properties/ for @observable

1 Like

How do I add a debounce behavior into my property observer subscription?

You have at least two following options:

  • using lodash debouce (https://lodash.com/docs/4.17.5#debounce)

       this.subscription = this.bindingEngine
             .propertyObserver(this, 'observeMe')
             .subscribe(_.debounce(
               (newValue, oldValue) => { 
                 this.objectValueChanged(newValue, oldValue) 
               })
             );
    
  • or another ‘standalone’ debounce function

    function debounce(func, wait, immediate) {
	  var timeout;
	  return function() {
		var context = this, args = arguments;
		var later = function() {
			timeout = null;
			if (!immediate) func.apply(context, args);
		};
		var callNow = immediate && !timeout;
		clearTimeout(timeout);
		timeout = setTimeout(later, wait);
		if (callNow) func.apply(context, args);
      };
    };

and usage like above for lodash.

Hi, back on the subject of the property observer, is the behavior in the linked post expected or a bug?

https://stackoverflow.com/questions/55190930/aurelia-property-observer-not-being-disposed

Thanks, Donal

1 Like

Observer should just be disposed in your code, as you can see from this example https://codesandbox.io/s/ovvp7m05o5

  • On landing, clicking anywhere in the document will increase counter + log Click changed called
  • When navigating to page 2, clicking anywhere will increase counter + not log Click changed called

I think you accidentally override the observer reference in your class. The observer that was created once in constructor could never be referenced again to dispose properly.

1 Like

I’ve come up with this code to handle observing changes to my objects’ properties

How would I code this into a decorator? Are there any ways to improve this process?

Thanks!!

Al;

note: I pass in the local supports (this.) to aid in mocking

import { LogManager, all } from 'aurelia-framework';
const Console = LogManager.getLogger('sample-code');

import { inject, BindingEngine } from 'aurelia-framework';

export const myDataObjName = 'dataset-a';
export interface myDataObjInterface {
    independentContractor: string,
    independentContractorNote: string,
    meetingInTheShowroom: string,
    visitTheSite: string,
};

@inject(BindingEngine)
export class SampleElement {

    public subscriptions: Array < any > =[];

    public myDataObj: myDataObjInterface = {
        independentContractor: '',
        independentContractorNote: '',
        meetingInTheShowroom: '',
        visitTheSite: '',
    };

    constructor(
        public bindingEngine: BindingEngine
    ) {
    }

    // this is my standard deactivate, 
    // pushing any&all subscriptions for disposal on deactivation
    deactivate() {
        this.subscriptions.forEach(subscriber => {
            if (subscriber && subscriber.dispose) {
                subscriber.dispose()
            }
        });
    }

    activate(params, routeConfig) {
        this.observeObjectDataChanges('opportunity', params.oppId, this.myDataObj, myDataObjName, this.subscriptions, this.bindingEngine, this.myDataChangeHandler);
    }

    observeObjectDataChanges(dataTypeName: string, dataObjId: number, dataObj: any, dataObjName: string, localSubscriptions: Array<any>, localBindingEngine: BindingEngine, localChangeHandler: Function) {
        for (const propName in dataObj) {
        localSubscriptions.push(localBindingEngine
            .propertyObserver(dataObj, propName)
            .subscribe((newValue, oldValue) => {
            localChangeHandler({ dataTypeName, dataObjId, dataObjName }, propName, newValue, oldValue)
            }));
        }
    }

    myDataChangeHandler(metaInfo, propName, newValue, oldValue) {
        Console.debug(metaInfo, propName, 'changed to:', newValue, 'from:', oldValue);
    }
}
1 Like