Observers

Rich Harris edited this page Jan 6, 2013 · 4 revisions

WikiObservers

Everything in Statesman.js is observable, meaning you can attach callbacks to the model that fire when the part we're interested in changes.

Let's create an empty model:

var state = new Statesman();

You can observe foo with

state.observe( 'foo', function ( newValue, oldValue ) {
    alert( 'foo changed from ' + oldValue + ' to ' + newValue );
});

state.set( 'foo', 'bar' ); // alerts 'foo changed from undefined to bar'
state.set( 'foo', 'baz' ); // alerts 'foo changed from bar to baz'

state.observe returns an array of observers, which can be passed to state.unobserve to cancel themselves:

var observers = state.observe( 'foo', function ( newValue, oldValue ) {
    alert( newValue );
});

state.unobserve( observers );

state.set( 'foo', 'bar' ); // does nothing - the observer is not notified

(Boring technical detail: it returns an array, because under the hood state.observe sets up observers for the specified keypath, and each of the upstream keypaths. Hence state.observe( 'foo.bar.baz[0]', callback ) will return an array of four observers - one for 'foo.bar.baz[0]', one for 'foo.bar.baz', one for 'foo.bar', and one for 'foo'. Don't worry though - the specified callback is shared by all the observers, it doesn't create anonymous wrapper functions or do anything else that wastes memory.)

Controlling when observers are notified

Ordinarily, you only want to be notified when something has changed, and so when state.set is called, observers are notified on that basis. However you can force an observer to be notified like so:

state = new Statesman({ foo: bar });

state.observe( 'foo', alert );

state.set( 'foo', 'bar' ); // does nothing
state.set( 'foo', 'bar', { force: true }); // alerts 'bar' even though it hasn't changed

Conversely, there may be occasions when you want to update the model without notifying observers:

state.set( 'foo', 'baz', { silent: true }); // does nothing, even though it HAS changed

You can't have both silent and force set to true - if you do, force will be ignored.

A word of caution: the equality check only applies to primitives (strings, numbers, booleans etc) - not to objects and arrays, which will always trigger notifications:

state = new Statesman({ foo: myArray });
state.observe( 'foo', alert );

state.set( 'foo', myArray ); // alerts '[object Array]' even though it's the same array

This is because downstream keypaths of those objects and arrays may have changed even though it's the same 'chunk of memory' in the system, and deep equality checks are expensive.

Initializing your observers

Often, you may want your observers to trigger immediately, rather than waiting for the first time state.set gets called. You can make that happen by passing in true as the third argument to state.observe:

state.set( 'foo', 'bar' );

state.observe( 'foo', function ( newValue, oldValue ) {
    alert( newValue ); // alerts 'bar' immediately, because the third argument is true
}, true );

state.set( 'foo', 'baz' ); // alerts 'baz' as you'd expect

(Question: should this be the default behaviour? Answers on a postcard to @rich_harris please.)

Single-use observers

If you know that a value will only be set once (for example, after some data has been loaded via AJAX or whatever), it's good hygiene to remove your observers after it has happened. state.observeOnce gives us a convenient way to do so:

state.observeOnce( 'foo', function ( newValue, oldValue ) {
    alert( newValue );
});

state.set( 'foo', 'bar' ); // alerts 'bar'
state.set( 'foo', 'baz' ); // does nothing - the observer has already been removed

Next: computed values