can-compute
Version:
CanJS’s legacy way of composing observables. Use can-observation instead.
373 lines (273 loc) • 10.5 kB
Markdown
-compute
can-observables
can-legacy
1.1
../docco/compute/compute.html docco
../package.json
Create an observable value.
`compute(getterSetter[, context])`
Create a compute that derives its value from other observables. Uses [can-observation] to call the `getterSetter` and track observables.
```js
const age = compute( 32 );
const nameAndAge = compute( function() {
return "Matthew - " + age();
} );
console.log( nameAndAge() ); // -> Matthew - 32
age( 33 );
console.log( nameAndAge() ); // -> Matthew - 33
```
{function(*+,*+)} getterSetter(newVal,oldVal) A function that gets, and optionally sets, the value of the compute. When called with no arguments, _getterSetter_ should return the current value of the compute. When called with a single argument, _getterSetter_ should arrange things so that the next read of the compute produces that value. This compute will automatically update its value when any observables values are read.
{Object} [context] The `this` to use when calling the `getterSetter` function.
{can-compute.computed} A new compute.
`compute(initialValue [, settings])`
Creates a compute from a value and optionally specifies how to read, update, and
listen to changes in dependent values. This form of compute can be used to
create a compute that derives its value from any source.
{*} initialValue The initial value of the compute. If `settings` is
not provided, the compute simply updates its value to whatever the first argument
to the compute is.
var age = compute(30);
age() //-> 30
age(31) //-> fires a "change" event
{can-compute.computeSettings} [settings]
Configures all behaviors of the [can-compute.computed]. The following cross
binds an input element to a compute:
```js
const input = document.getElementById( "age" );
const value = compute( "", {
get: function() {
return input.value;
},
set: function( newVal ) {
input.value = newVal;
},
on: function( updated ) {
input.addEventListener( "change", updated, false );
},
off: function( updated ) {
input.removeEventListener( "change", updated, false );
}
} );
```
{can-compute.computed} The new compute.
`compute(initialValue, setter(newVal,oldVal))`
Create a compute that has a setter that can adjust incoming new values.
```js
const age = compute( 6, function( newVal, oldVal ) {
if ( !isNaN( +newVal ) ) {
return +newVal;
} else {
return oldVal;
}
} );
```
{*} initialValue
The initial value of the compute.
{function(*,*):*} setter(newVal,oldVal)
A function that is called when a compute is called with an argument. The function is passed
the first argumented passed to [can-compute.computed] and the current value. If
`set` returns a value, it is used to compare to the current value of the compute. Otherwise,
`get` is called to get the current value of the compute and that value is used
to determine if the compute has changed values.
{can-compute.computed} A new compute.
`compute(object, propertyName [, eventName])`
Create a compute from an object's property value. This short-cut
signature lets you create a compute on objects that have events
that can be listened to with [can-compute.computed.on].
```js
const input = document.getElementById( "age" );
const age = compute( input, "value", "change" );
const me = new DefineMap( { name: "Justin" } );
const name = compute( me, "name" );
```
{Object} object An object that has an `addEventListener` method and events dispatched on it.
{String} propertyName The property value to read on `object`. The
property will be read via `object.attr(propertyName)` or `object[propertyName]`.
{String} [eventName=propertyName] Specifies the event name to listen
to on `object` for `propertyName` updates.
{can-compute.computed} A new compute.
## Use
`can-compute` exports a function that lets you make an observable value. The following
makes an observable `age` compute whose value changes from `33` to `34`:
```js
import compute from "can-compute";
const age = compute( 33 );
age(); // 33
age.on( "change", function( ev, newVal, oldVal ) {
newVal; //-> 34
oldVal; //-> 33
} );
age( 34 );
age(); // 33
```
Computes are similar
to observable maps like [can-define/map/map], but they represent a single value rather than a collection of values.
Computes can derive their value from other computes, maps and lists.
When the derived values change, the compute's value will be automatically updated. This
is `can-compute`'s best feature. For example, the following combines the age
compute in the previous example, and a `name` compute into an `info` compute:
```js
const age = compute( 33 ),
name = compute( "Justin" ),
info = compute( function() {
return name() + " is " + age() + ".";
} );
info(); //-> "Justin is 33."
```
If we listen to [can-compute.computed.ChangeEvent] on `info`, if either `age` or `name`
changes, `info` will be updated automatically:
```js
info.on( "change", function( ev, newVal, oldVal ) {
newVal; //-> "Justin is 34."
} );
age( 34 );
```
Computes are similar to event streams like `Bacon.js` or `RXJS`. However, computes
are easier to compose values because:
- you can just read other observables and computes and return a value.
- you don't have to manage subscribing and merging streams yourself.
Also, computes can also have [can-event/batch/batch batched updates] to prevent unnecessary
updates. For example, if both `age` and `name` were changed at the same time, we
could prevent `info` from updating twice with:
```js
import canBatch from "can-event/batch/batch";
canBatch.start();
age( 35 );
name( "Justin Meyer" );
canBatch.stop();
```
There are a wide variety of ways to create computes. Read on to understand the basics.
## Observing a value
The simplest way to use a compute is to have it store a single value, and to set it when
that value needs to change:
```
var tally = compute(12);
tally(); // 12
tally.on("change",function(ev, newVal, oldVal){
console.log(newVal,oldVal)
})
tally(13);
tally(); // 13
```
Any value can be observed. The following creates a compute
that holds an object and then changes it to an array.
```js
const data = compute( { name: "Justin" } );
data( [ { description: "Learn Computes" } ] );
```
## Derived computes
If you use a compute that derives its
value from properties of an observable map or other [can-compute]s, the compute will listen for changes in those
properties and automatically recalculate itself, emitting a _change_ event if its value
changes.
The following example shows creating a `fullName` compute
that derives its value from two properties on the `person` observe:
```js
const person = new Person( {
firstName: "Alice",
lastName: "Liddell"
} );
const fullName = compute( function() {
return person.firstName + " " + person.lastName;
} );
fullName.on( "change", function( ev, newVal, oldVal ) {
console.log( "This person's full name is now " + newVal + "." );
} );
person.firstName = "Allison"; // The log reads:
//-> "This person's full name is now Allison Liddell."
```
Because Person is an observable [can-define/map/map] can-compute knows to listen for changes because the map's firstName and lastName properties are read.
## Translator computes - computes that update their derived values
Sometimes you need a compute to be able to translate one value to another. For example,
consider a widget that displays and allows you to update the progress in percent
of a task. It accepts a compute with values between 0 and 100. But,
our task observe has progress values between 0 and 1 like:
```js
const task = new DefineMap( {
progress: 0.75
} );
```
Use `compute( getterSetter )` to create a compute that updates itself
when task's `progress` changes, but can also update progress when
the compute function is called with a value. For example:
```js
const progressPercent = compute( function( percent ) {
if ( arguments.length ) {
task.progress = percent / 100;
} else {
return task.progress * 100;
}
} );
progressPercent(); // -> 75
progressPercent( 100 );
task.progress; // -> 1
```
The following is a similar example that shows converting feet into meters and back:
```js
const wall = new DefineMap( {
material: "brick",
length: 10 // in feet
} );
const wallLengthInMeters = compute( function( lengthInM ) {
if ( arguments.length ) {
wall.length = lengthInM * 3.28084;
} else {
return wall.length / 3.28084;
}
} );
wallLengthInMeters(); // 3.048
// When you set the compute...
wallLengthInMeters( 5 );
wallLengthInMeters(); // 5
// ...the original map changes too.
wall.length; // 16.4042
```
## Events
When a compute's value is changed, it emits a [can-compute.computed.ChangeEvent] event. You can listen for this change
event by using `on` to bind an event handler to the compute:
```js
const tally = compute( 0 );
tally.on( "change", function( ev, newVal, oldVal ) {
console.log( "The tally is now at " + newVal + "." );
} );
tally( tally() + 5 ); // The log reads:
// 'The tally is now at 5.'
```
## Caching values
A compute that has an event listener will cache its value and only update when one of its source observables change.
For example:
```js
const foo = {
first: "Wonder"
};
const last = compute( "Woman" );
const hero = {
fullName: compute( function() {
return foo.first + " " + last();
} )
};
hero.fullName.on( "change", function() {} ); // bind to compute
console.log( hero.fullName() ); // console.logs "Wonder Woman"
foo.first = "Super";
console.log( hero.fullName() ); // console.logs "Wonder Woman" because the source observable (last) hasn't changed
last( "Man" );
console.log( hero.fullName() ); // console.logs "Super Man" because fullName updates its value now after hearing the change on "last"
```
In contrast, if we didn't bind to the compute:
```js
const foo = {
first: "Wonder"
};
const last = compute( "Woman" );
const hero = {
fullName: compute( function() {
return foo.first + " " + last();
} )
};
console.log( hero.fullName() ); // console.logs "Wonder Woman"
foo.first = "Super";
console.log( hero.fullName() ); // console.logs "Super Woman" because fullName did not cache its previous value
last( "Man" );
console.log( hero.fullName() ); // console.logs "Super Man"
```
{function} can