Rename observable to prop(erty) to distinguish from TC39

This commit is contained in:
Timothy Farrell 2017-11-09 16:59:45 -06:00
parent 4f4747536b
commit 914779fb44
8 changed files with 67 additions and 74 deletions

View File

@ -1,21 +1,13 @@
# FRP tools
Observer and Computed value stores designed to work together for storing real state and derived
state.
Property and Computed value stores designed to work together for storing real and derived state.
# [observable](./src/observable.js)
# [property](./src/property.js)
`observable` is a simple value store that can report when its value changes. It is good for wrapping
external props passed into a component so compute types can dependent on them. It can also be used
to receive events such as _window.onresize_ to always provide the current viewport size.
NOTE: Javascript has a proposal for a thing called an
[Observable](https://github.com/tc39/proposal-observable). This is not an implementation of that.
They do serve similar functions (provide a way to communicate when a value changes) but the tc39
proposal is more about event input sources and can communicate errors and be extended. This
implementation is designed to be as small and simple as possible. Extending it is done via
[Computed] instances depending on them. I may rename `observable` to `property` in a future major
release to avoid this confusion.
A `property` is a simple value store that can report when its value changes. It is good for wrapping
external values passed into a component so compute types can dependent on them and only recompute
when these values change. It can also be used to receive events such as _window.onresize_ to always
provide the current viewport size.
## Usage
@ -24,7 +16,7 @@ release to avoid this confusion.
Creates and sets initial value to `true`
```js
const inViewport = observable(true);
const inViewport = prop(true);
```
### Read
@ -48,15 +40,15 @@ inViewport(false);
### Subscribe to changes
Call the `subscribe` method with a callback that will be called when the observable is changed to a
different value. The returned function can be called to unsubscribe from the observable. When called
it will provide the count of remaining subscriptions.
Call the `subscribe` method with a callback that will be called when the property value changes. The
returned function can be called to unsubscribe from the property. When called it will provide the
count of remaining subscriptions.
```js
const unsubscribe = inViewport.subscribe(console.log.bind(console));
const remainingSubscriptionCount = unsubscribe();
inViewport.unsubscribeAll(); // Call unsubscribeAll to remove child observables/computeds.
inViewport.unsubscribeAll(); // Call unsubscribeAll to remove child property/computed subscriptions.
```
### Provide a comparator for complex types
@ -74,13 +66,13 @@ function setEquals(a, b) {
);
}
const a = observable(new Set([1, 2]), setEquals);
const a = prop(new Set([1, 2]), setEquals);
```
# [computed](./src/computed.js)
`computed` is a functional store that depends on the values of observables or other computeds. They
derive value from observables rather than store value and hence cannot be set directly.
`computed` is a functional store that depends on the values of properties or other computeds. They
derive value from properties rather than store value and hence cannot be set directly.
## Behavior
@ -95,7 +87,7 @@ is set, otherwise it just return the stored result from the last time it compute
```js
const showDialog = computed(
(inVP, shouldShow) => inVP && shouldShow, // computation function
[inViewport, shouldShow] // array of dependencies, can be either observable or computed
[inViewport, shouldShow] // array of dependencies, can be either a property or computed
);
```
@ -112,7 +104,7 @@ Call it to receive the stored value, recomputing if necessary.
### Subscribe to changes
Call the subscribe method with a callback that will be called when the computed result changes to a
different value. The returned function can be called to unsubscribe from the observable. When called
different value. The returned function can be called to unsubscribe from the property. When called
it will provide the count of remaining subscriptions.
```js
@ -125,8 +117,8 @@ changes. This could negatively performance if it depends on multiple values that
and the computation function is non-trivial. For example:
```js
const inViewport = observable(false);
const shouldShow = observable(false);
const inViewport = prop(false);
const shouldShow = prop(false);
const showDialog = computed((inVP, shouldShow) => inVP && shouldShow, [inViewport, shouldShow]);
@ -142,7 +134,7 @@ shouldShow(false); // showDialog result recomputed, console.log is not called.
showDialog(); // showDialog does not recompute, console.log is not called. `false` is returned.
showDialog.detach(); // Call detach to remove this computed from the logic tree.
showDialog.unsubscribeAll(); // Call unsubscribeAll to remove child observables/computeds.
showDialog.unsubscribeAll(); // Call unsubscribeAll to remove child property/computed subscriptions.
```
### Provide a comparator for complex types
@ -164,25 +156,26 @@ function _intersection(a, b) {
return new Set([...a].filter(x => b.has(x)));
}
const a = observable(new Set([1, 2]), setEquals);
const b = observable(new Set([2, 3]), setEquals);
const a = prop(new Set([1, 2]), setEquals);
const b = prop(new Set([2, 3]), setEquals);
const intersection = computed(_intersection, [a, b], setEquals);
```
# [bundle](./src/bundle.js)
`bundle` is a wrapper around a group of `observables` for the purpose of applying changes to all of
them at once without having to trigger a subscription that may depend on more than observable in the
`bundle` is a wrapper around a group of properties for the purpose of applying changes to all of
them at once without having to trigger a subscription that may depend on more than property in the
group.
Another way to think of a `bundle` is an `observable` that takes an object and exposes the
properties as individual observables.
Another way to think of a `bundle` is a `property` that takes an object and exposes the object's
properties as individual `property` instances.
## Behavior
A `bundle` wraps observables to intercept dependency hooks in such a way that updating all
observables can happen at once before any downstream `computeds` are evaluated. A bundle returns a
function that can be called with an object to set values for the mapped member observables.
A `bundle` wraps properties to intercept dependency hooks in such a way that updating all `property`
instances can happen at once before any downstream `computed` instances are evaluated. A bundle
returns a function that can be called with an object to set values for the mapped member `property`
instances.
## Usage
@ -190,14 +183,14 @@ function that can be called with an object to set values for the mapped member o
```js
const layoutEventBundle = bundle({
width: observable(1),
height: observable(2)
width: prop(1),
height: prop(2)
});
const ratio = computed((a, b) => a / b, [layoutEventBundle.width, layoutEventBundle.height]);
ratio.subscribe(render);
```
### Change Member Observables atomically
### Change Member Properties atomically
```js
layoutEventBundle({ width: 640, height: 480 });
@ -207,11 +200,11 @@ layoutEventBundle({ width: 640, height: 480 });
change. But bundle allows both values to change and `ratio` will only be evaluated once and `render`
called once.
### Change Member Observables individually
### Change Member Properties individually
```js
layoutEventBundle.width(640);
layoutEventBundle.height(480);
```
The observables exposed by the bundle can also be updated apart from their grouping.
The properties exposed by the bundle can also be updated apart from their grouping.

View File

@ -1,7 +1,7 @@
{
"name": "frptools",
"version": "1.2.0",
"description": "Observable and Computed data streams",
"version": "2.0.0",
"description": "Observable Property and Computed data streams",
"main": "lib/index.js",
"jsnext:main": "src/index.js",
"files": ["dist", "lib", "src"],

View File

@ -1,4 +1,4 @@
const { observable, computed, bundle } = require('../lib/index.js');
const { prop, computed, bundle } = require('../lib/index.js');
describe('bundle', () => {
const methods = {
@ -13,10 +13,10 @@ describe('bundle', () => {
spyOn(methods, 'getVal').and.callThrough();
});
it('bundles observable changes together', () => {
it('bundles property changes together', () => {
const a = bundle({
a: observable(0),
b: observable(10)
a: prop(0),
b: prop(10)
});
const b = computed(methods.square, [a.a]);
const c = computed(methods.add, [a.a, a.b]);
@ -47,8 +47,8 @@ describe('bundle', () => {
});
it('unbundled changes are less efficient', () => {
const a = observable(0);
const _b = observable(10);
const a = prop(0);
const _b = prop(10);
const b = computed(methods.square, [a]);
const c = computed(methods.add, [a, _b]);
@ -80,8 +80,8 @@ describe('bundle', () => {
it('allows individual members to be updated', () => {
const a = bundle({
a: observable(0),
b: observable(10)
a: prop(0),
b: prop(10)
});
const b = computed(methods.square, [a.a]);
const c = computed(methods.add, [a.a, a.b]);

View File

@ -1,11 +1,11 @@
const { observable, computed } = require('../lib/index.js');
const { prop, computed } = require('../lib/index.js');
describe('computed', () => {
const add = (a, b) => a + b;
const square = a => a * a;
it('returns the value computed from its dependencies', () => {
const a = observable(0);
const a = prop(0);
const b = computed(square, [a]);
const c = computed(add, [a, b]);
@ -28,7 +28,7 @@ describe('computed', () => {
it('only computes when called', () => {
let runCount = 0;
let currentValue = 1;
const a = observable(0);
const a = prop(0);
const b = computed(
val => {
runCount += 1;
@ -59,7 +59,7 @@ describe('computed', () => {
let runCount = 0;
let subRunCount = 0;
let currentValue = 1;
const a = observable(0);
const a = prop(0);
const b = computed(
val => {
runCount += 1;
@ -99,7 +99,7 @@ describe('computed', () => {
let runCount = 0;
let subRunCount = 0;
let currentValue = 1;
const a = observable(0);
const a = prop(0);
const b = computed(
val => {
runCount += 1;
@ -142,7 +142,7 @@ describe('computed', () => {
});
it('can be detached', () => {
const a = observable(2);
const a = prop(2);
const b = computed(square, [a]);
const c = computed(add, [a, b]);
@ -173,8 +173,8 @@ describe('computed', () => {
return new Set([...a].filter(x => b.has(x)));
}
const a = observable(new Set([1, 2]), setEquals);
const b = observable(new Set([2, 3]), setEquals);
const a = prop(new Set([1, 2]), setEquals);
const b = prop(new Set([2, 3]), setEquals);
const ABintersection = computed(intersection, [a, b], setEquals);
expect(runCount).toEqual(0);

View File

@ -1,13 +1,13 @@
const { observable } = require('../lib/index.js');
const { prop } = require('../lib/index.js');
describe('observable', () => {
describe('A property', () => {
it('returns its initialized value', () => {
const a = observable(true);
const a = prop(true);
expect(a()).toEqual(true);
});
it('returns its set value', () => {
const a = observable();
const a = prop();
expect(a()).toEqual(undefined);
expect(a(true)).toEqual(true);
});
@ -15,7 +15,7 @@ describe('observable', () => {
it('returns notifies dependents of updates', () => {
let runCount = 0;
let currentValue = 1;
const a = observable();
const a = prop();
a.subscribe(val => {
runCount += 1;
expect(val).toEqual(currentValue);
@ -39,7 +39,7 @@ describe('observable', () => {
it('honors cancelled subscriptions', () => {
let runCount = 0;
let currentValue = 1;
const a = observable();
const a = prop();
const cancelSubscription = a.subscribe(val => {
runCount += 1;
expect(val).toEqual(currentValue);
@ -74,7 +74,7 @@ describe('observable', () => {
let runCount = 0;
const a = observable(new Set([1, 2]), setEquals);
const a = prop(new Set([1, 2]), setEquals);
a.subscribe(() => (runCount += 1));
expect([...a()]).toEqual([1, 2]);
expect(runCount).toEqual(0);

View File

@ -1,4 +1,4 @@
export function bundle(observables) {
export function bundle(props) {
const activeSubscribers = new Set();
let activeUpdate = false;
@ -6,9 +6,9 @@ export function bundle(observables) {
const result = {};
activeUpdate = true;
Object.keys(values)
.filter(k => typeof observables[k] === 'function')
.filter(k => typeof props[k] === 'function')
.forEach(k => {
result[k] = observables[k](values[k]);
result[k] = props[k](values[k]);
});
const subscribers = Array.from(activeSubscribers);
@ -32,8 +32,8 @@ export function bundle(observables) {
});
};
Object.keys(observables).forEach(k => {
const obs = observables[k];
Object.keys(props).forEach(k => {
const obs = props[k];
accessor[k] = obs;
obs._d = subscriptionFactory(obs._d);

View File

@ -1,3 +1,3 @@
export { observable } from './observable';
export { prop } from './property';
export { computed } from './computed';
export { bundle } from './bundle';

View File

@ -1,9 +1,9 @@
import { eq } from './util.js';
export function observable(store, comparator = eq) {
export function prop(store, comparator = eq) {
const subscribers = new Set();
const accessor = function _observable(newVal) {
const accessor = function _prop(newVal) {
if (newVal !== undefined && !comparator(store, newVal)) {
store = newVal;
subscribers.forEach(s => s(store));