Computed Properties

edit this page

The idea is fairly simple: you can define computed properties that update reactively based on their dependencies. In previous versions you may have done something as follows.

ractive = new Ractive({
  el: 'body',
  template: '{{width}} * {{height}} = {{ area() }}', // note the function invocation
  data: {
    width: 100,
    height: 100,
    area: function () { return this.get( 'width' ) * this.get( 'height' ); }
  }
});

That's nice and all - the {{ area() }} mustache updates reactively as width and height change - but it's limited. To get the area value programmatically you'd have to do something like...

area = ractive.get('area').call(ractive);

...which effectively prevents you from composing computed values together in any meaningful way. And you can't 'observe' the area outside of the template, without doing something like this:

ractive.observe( 'width height', function () {
    var area = this.get( 'width' ) * this.get( 'height' );
    doSomething( area );
});

Computed properties to the rescue

Now, you can do

ractive = new Ractive({
  el: 'body',
  template: '{{width}} * {{height}} = {{area}}', // `area` looks like a regular property
  data: {
    width: 100,
    height: 100
  },
  computed: {
    area: function () { return this.get( 'width' ) * this.get( 'height' ); }
  }
});

With this, the area property can be treated like any other. It will update reactively (because the calls to ractive.get() tell Ractive that it should be recomputed when width or height change), so you can do...

ractive.observe( 'area', doSomething );

...instead of manually recalculating it. And computed values can depend on other computed values, and so on (before anyone asks, we're not doing a topological sort or anything fancy like that - in real world scenarios I'd expect the overhead of doing the sort to be greater than the cost of occasionally recomputing a node in the dependency graph more times than is required).

Compact syntax

The syntax used above, where each computed property is defined as a function, gives you a lot of flexibility. But there's a more compact string syntax you can use:

ractive = new Ractive({
  ...,
  computed: {
    area: '${width} * ${height}'
  }
});

This string is turned into a function with the Function constructor (which unfortunately means it isn't CSP compliant) - any ${...} blocks are basically turned into ractive.get('...'), so it works exactly the same way. Needless to say you can use any JavaScript here - ${foo}.toUpperCase(), Math.round(${num}), and so on.

Setting computed values

By default, computed values are read-only, and if you try to ractive.set('someComputedProperty') an error will be thrown. But you can use a third syntax option which allows you to declare a set() method:

ractive = new Ractive({
  data: { firstname: 'Douglas', lastname: 'Crockford' },
  computed: {
    fullname: {
      get: '${firstname} + " " + ${lastname}', // or use the function syntax
      set: function ( fullname ) {
        var names = fullname.split( ' ' );

        this.set({
          firstname: names[0] || '',
          lastname: names[1] || ''
        });
      }
    }
  }
});

ractive.set( 'fullname', 'Rich Harris' );

ractive.get( 'firstname' ); // Rich
ractive.get( 'lastname' ); // Harris

Components

You can, of course, declare computed values on components:

Box = Ractive.extend({
  template: boxTemplate,
  computed: { area: '${width} * ${height}' }
});

box = new Box({
  ...,
  data: { width: 20, height: 40 }
});

box.get( 'area' ); // 800

Additional computed properties can be declared on the instance:

box2 = new Box({
  ...,
  data: { width: 20, height: 40, depth: 60 },
  computed: { volume: '${area} * ${depth}' }
});

box2.get( 'area' ); // 800
box2.get( 'volume' ); // 48000

Data context for computed properties

Computed properties can only be calculated for the instance context as a whole. You can't, for example, directly compute a value for each member of an array:

new Ractive({
  template: '{{#boxes}}{{area}}{{/}}',
  data: {
    boxes: [
      { width: 20, height: 40 },
      { width: 30, height: 45 },
      { width: 20, height: 20 }
    ]
  },
  // there's no way to specify this for "each" box :(
  computed: { area: '${width} * ${height}' }
});

The solution is to either use a function that calculates the value for each member:

  template: '{{#boxes:b}}{{ getArea(b) }}{{/}}',
  data: {
    boxes: [
      { width: 20, height: 40 },
      { width: 30, height: 45 },
      { width: 20, height: 20 }
    ],
    getArea: function ( i ) {
      var box = this.get( 'boxes.' + i );
      return box.width * box.area;
    }
  }

Or leverage a component to "scope" the data to each item:

Box = Ractive.extend({
  template: boxTemplate,
  computed: { area: '${width} * ${height}' }
});

new Ractive({
  template: '{{#boxes}}<box/>{{/}}',
  data: {
    boxes: [
      { width: 20, height: 40 },
      { width: 30, height: 45 },
      { width: 20, height: 20 }
    ]
  },
  components: { box: Box }
});