Skip to content

Observables

ggmod edited this page Mar 30, 2016 · 1 revision

#Observables

Observables enable you to listen to model changes, so that modifying the model will automatically trigger the necessary changes in the view elements.

You can create observables from objects, arrays, or primitive values.

Observable object

var person = d3bind.observable({ name: 'John Smith', age: 23 });

person.$name.subscribe((name, oldValue) => {
	console.log('Name changed from ' + oldValue + ' to ' + name);
});

console.log(person.name); // 'John Smith'
person.name = 'James Smith'; // 'Name changed from John Smith to James Smith'
console.log(person.name); // 'James Smith'

Transforming a simple JavaScript object into an observable adds the extra properties prefixed with '$', that can be used to listen to a given property's changes. The $name and $age properties added to the observable object in the above example satisfy the Observable interface:

interface Observable<T> {
    get() : T,
    subscribe(handler: ObservableHandler<T>): () => boolean,
    unsubscribe(handler: ObservableHandler<T>): boolean,
    unsubscribeAll(): number,
    trigger(): void
}

type ObservableHandler<T> = (newValue: T, oldValue: T, caller?: any) => void;

Calling person.$age.get() in this case would be equivalent to just writing person.age.

You can unsubscribe listeners from the observable with the unsubscribe and unsubsribeAll functions, or by calling the function returned by subscribe(). (If you try to unsubscribe a handler that wasn't subscribed in the first place, the unsubscribe function just returns false, it doesn't throw an error.)

The observable properties of the model object also implement the writeable observable interface:

export interface WritableObservable<T> extends Observable<T> {
    set(value: T, noTrigger?: boolean, caller?: any): void
}

Running model.age = 42 triggers the registered listeners, but by calling model.$age.set(42, true) the handlers of the subscribers won't be triggered. You can also pass in an optional caller parameter, that will be passed to all the subscribers.

Observable value

It's possible to create an observable of a single value, but in this case one can't avoid using the verbose .get() and .set() methods. The ObservableValue satisfies the WritableObservable interface, just like the $name and $age properties of the observable object in the previous example.

var size = d3bind.observable(100);
size.subscribe((value, oldValue) => {
	console.log('size changed from ' + oldValue + ' to ' + value)
});
console.log(size.get()); // 100
size.set(200); // size changed from 100 to 200 
console.log(size.get()); // 200

If you want to create an observable value from an object instead of a primitive value, then you have to use the ObservableValue class, because the d3bind.observable() function makes the properties of an object observable, not the object itself:

var person = new d3bind.ObservableValue({ name: 'John Smith', age: 32 });
person.subscribe((person) => { console.log('Person replaced', person); });
person.set({ name: 'James Smith', age: 20 }); // Person replaced: { name: 'James Smith', age: 20 }

If you are using npm and ES6 modules, then you can also access the ObservableValue class by:

import {ObservableValue} from 'd3bind';

var size = new ObservableValue(100);

Observable Array

Observable arrays have most of the methods of JavaScript arrays, and some extra features. However it's not possible to access the elements of the array with the usual t[0] syntax, because JavaScript arrays can't be extended properly in ECMAScript 5 (the version of JavaScript that all major browsers support), you will have to use the .get(index) and .set(index, value) methods.

var array = d3bind.observable([1,2,3]);
array.subscribe({
	insert: (item, index) => {
		console.log('item inserted:', item, 'at index:', index);
	},
	remove: (item, index) => {
		console.log('item removed:', item, 'from index:', index);
	}
});

array.push(4); // item inserted: 4 at index: 3
array.splice(2, 1); // item removed: 3 from index: 2
console.log(array.toString()); // 1,2,4
console.log(array.length); // 3

// element access instead of array[i]:
console.log(array.get(2)); // 4
array.set(1, 10); // item removed: 2 from index: 1 , item inserted: 10 at index: 1

As the example above show, replacing the item at a given index triggers a remove event, and then an insert event. To avoid this, it's possible to subscribe to an optional replace event too:

var array = d3bind.observable([1,2,3]);
array.subscribe({
	insert: (item, index) => {
		console.log('item inserted:', item, 'at index:', index);
	},
	remove: (item, index) => {
		console.log('item removed:', item, 'from index:', index);
	},
	replace: (item, index, oldValue) => {
		console.log('item changed from', oldValue, 'to', item, 'at index', index);
	}
});
array.set(1, 10); // item changed from 2 to 10 at index 1

Aside from the insert/remove/replace events, the observable array also has some observable fields, all of which implement the Observable interface:

  • $length: implements the Observable<number> interface, subscribes to changes of the array's length
  • $index(index) function, that subscribes to the changes at the given index of the array
  • $all() - subscribes to all the insert/remove events, but using the Observable interface instead of the array's change handlers
  • $all(accessor) subscribes to an observable of each array item, for example, if the array contains observable objects with a name property, then $all(item => item.$name) will be triggered if any of the names change.

You can initialize an observable array by binding it to another one, this way the second array will always get updated when the original changes:

var a1 = new d3bind.ObservableArray([1, 2, 3]);
var a2 = d3bind.ObservableArray.bindTo(a1, item => item * item); // a2 is [1, 4, 9]
a1.push(4); // a2 is [1, 4, 9, 16]

Similiarly to the ObservableValue, the array can be imported from the global d3bind object or with the module syntax:

var array = new d3bind.ObservableArray([1,2,3]);
// or:
import {ObservableArray} from 'd3bind';
var array = new ObservableArray([1,2,3]);

Observable Set, Map

There is also on ObservableSet and an ObservableMap based on the API-s of the ES6 collections, but they are implemented using ES5, so only string or number values work as keys.

They behave similarly to the ObservableArray, and have most of the same features. You can listen to collection's length on the observable $size property, subscribe to all the items with $äll(..), or to a given key with $has(item) (on ObservableSet) or $key(key) (on ObservableMap). You can also initialize a set or a map with binding to an observable array.

Deep observable hierarchy

You can easily create deep observable hierarchies (for example observable objects in an observable list, or the other way) with the deepObservable function. For example:

// persons will be an ObservableArray, and its elements observable objects with the $age and $name properties added
var persons = d3bind.deepObservable([{ name: 'James', age: 32 }, {name: 'Mary', age: 22}]);
// don't forget to convert the new items to observables before you insert them:
persons.push(d3bind.observable({ name: 'John', age: 22 });

Clone this wiki locally