Computed values

Rich Harris edited this page Apr 21, 2013 · 7 revisions

WikiComputed values

Sometimes what you're really interested in is a second-order property of your data - a combined string, an average, or a total, or something else entirely.

Statesman.js lets you create computed values using state.compute. Note that the value of this is the state instance:

state = new Statesman({
    firstname: 'Winston',
    lastname: 'Churchill'
});

state.compute( 'fullname', {
    fn: function () {
        return this.get( 'firstname' ) + ' ' + this.get( 'lastname' );
    }
});

alert( state.get( 'fullname' ) ); // alerts 'Winston Churchill'

That's all well and good, but a) the full name will be recomputed every time you call state.get( 'fullname' ), and b) observers of fullname won't be notified when it changes. We can fix that.

Computed values with triggers

We know when fullname will change in the example above - when either firstname or lastname do. We can describe that in code - note that fn is now passed the current values of its triggers as arguments:

state.compute( 'fullname', {
    triggers: [ 'firstname', 'lastname' ],
    fn: function ( firstname, lastname ) {
        return firstname + ' ' + lastname;
    }
});

Now, we can observe fullname and be notified of its changed value when firstname and/or lastname change (if both are set simultaneously, the observer will only be notified once):

state.observe( 'fullname', function ( fullname ) {
    alert( fullname );
});

state.set({
    firstname: 'Nelson',
    lastname: 'Mandela'
}); // alerts 'Nelson Mandela'

Computed values and caching

Because we've specified that fullname is dependent on firstname and lastname, Statesman.js does some work for us: when firstname or lastname change, it recalculates fullname and caches the value. Then, when you do state.get( 'fullname' ), it refers to the cache rather than recomputing it.

Most of the time this is exactly what you want. But there are some situations - namely, where the computed value depends on things other than its triggers. In the example below, we explicitly disable caching so that the value is updated each time we get it:

state = new Statesman({
    startTime: +new Date()
});

// note below that you can use 'trigger' and 'triggers' interchangeably. If there is only one trigger,
// you don't have to wrap the name in an array - just pass in a string if you want
state.compute( 'elapsed', {
    trigger: 'startTime',
    fn: function ( startTime ) {
        return Math.floor( ( +new Date() - startTime ) / 1000 );
    },
    cache: false
});

markTime = function () {
    console.log( 'Seconds since start: ', state.get( 'elapsed' ) );
};

setInterval( markTime, 1000 ); // will count 1, 2, 3, 4... each second

Observers of elapsed will only be notified when startTime changes, in this example - not when it is recomputed.

Note that computed values without triggers are never cached.

Overriding computed values

Ordinarily, computed values are read only. For example, this will throw an error:

state = new Statesman({
    name: {
        first: 'Benjamin',
        last: 'Disraeli'
    }
});

state.compute( 'fullname', {
    triggers: [ 'name' ],
    fn: function ( name ) {
        return name.first + ' ' + name.last;
    }
});

state.set( 'fullname', 'Margaret Thatcher' ); // throws an error - 'fullname' is readonly

However in some situations you may want to override a computed value. To do so set readonly to false:

state.compute( 'fullname', {
    triggers: [ 'name' ],
    fn: function ( name ) {
        return name.first + ' ' + name.last;
    },
    readonly: false
});

state.set( 'fullname', 'Margaret Thatcher' );
alert( state.get( 'fullname' ) ); // alerts 'Margaret Thatcher'

As soon as the computed value's triggers are updated, it reverts:

state.set( 'name', { first: 'Cecil', last: 'Rhodes' });
alert( state.get( 'fullname' ) ); // alerts 'Cecil Rhodes'

A neat (but probably inadvisable) trick

Computed values that aren't readonly can have a bi-directional relationship. Note that in this example we are creating two computed values with a single call to state.compute:

state.compute({
    // Compute fullname based on name...
    fullname: {
        triggers: [ 'name' ],
        fn: function ( name ) {
            return name.first + ' ' + name.last;
        },
        readonly: false
    },

    // ...and vice versa...
    name: {
        triggers: 'fullname',
        fn: function ( fullname ) {
            var split = fullname.split( ' ' );
            return {
                first: split[0],
                last: split[1]
            };
        },
        readonly: false
    }
});

// ...so you can update your data from different angles without
// descending into the infinite loop vortex of hell
state.set( 'fullname', 'Tony Blair' );
alert( state.get( 'name.first' ) ); // alerts 'Tony'

state.set( 'name.last', 'Benn' );
alert( state.get( 'fullname' ) ); // alerts 'Tony Benn'

I say inadvisable because if the computed values don't mirror each other exactly you'll probably eventually end up with some unexpected and hard to debug results. But it saves me a headache every now and then.

Next: Subsets