RactiveJS/Ractive — pull request #513 by Rich-Harris (2014-03-21 19:10:48+00:00)
Title: New feature: computed properties

I couldn't resist sneaking one more feature in before the upcoming 0.4.0 release, since it's something I need in my current project. In the spirit of the collaborative git workflow we're hammering out on the [new mailing list](https://groups.google.com/forum/#!topic/ractive-js/7HFPdWB-aRk) I'm submitting it as a PR to give everyone the opportunity to offer any feedback.

The idea is fairly simple: you can define computed properties that update reactively based on their dependencies. Previously, you might have done something like this:

```js
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 ridiculous like...

```js
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:

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

## Computed properties to the rescue

Now, you can do

```js
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...

```js
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:

```js
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](https://developer.mozilla.org/en-US/docs/Security/CSP)) - 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:

```js
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', 'Brendan Eich' );

ractive.get( 'firstname' ); // Brendan
ractive.get( 'lastname' ); // Eich
```

## Components

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

```js
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:

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

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

## Trying it out

As ever, would love to hear any feedback (especially if you encounter bugs etc). You can experiment with this feature by cloning the repo, checking out the `computed` branch, installing dependencies and building the project with grunt:

```shell
$ git clone https://github.com/RactiveJS/Ractive.git
$ cd Ractive
$ git fetch origin
$ git checkout -b computed origin/computed
$ npm i
$ grunt
```

You can then grab the `Ractive.js` from the `build` folder. Alternatively, you can experiment without building the library by using the `sandbox` - copy the `sandbox/sample` folder and edit the `index.html` file.