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 # FRP tools
Observer and Computed value stores designed to work together for storing real state and derived Property and Computed value stores designed to work together for storing real and derived state.
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 A `property` 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 external values passed into a component so compute types can dependent on them and only recompute
to receive events such as _window.onresize_ to always provide the current viewport size. when these values change. 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.
## Usage ## Usage
@ -24,7 +16,7 @@ release to avoid this confusion.
Creates and sets initial value to `true` Creates and sets initial value to `true`
```js ```js
const inViewport = observable(true); const inViewport = prop(true);
``` ```
### Read ### Read
@ -48,15 +40,15 @@ inViewport(false);
### Subscribe to changes ### Subscribe to changes
Call the `subscribe` method with a callback that will be called when the observable is changed to a Call the `subscribe` method with a callback that will be called when the property value changes. The
different value. The returned function can be called to unsubscribe from the observable. When called returned function can be called to unsubscribe from the property. When called it will provide the
it will provide the count of remaining subscriptions. count of remaining subscriptions.
```js ```js
const unsubscribe = inViewport.subscribe(console.log.bind(console)); const unsubscribe = inViewport.subscribe(console.log.bind(console));
const remainingSubscriptionCount = unsubscribe(); 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 ### 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](./src/computed.js)
`computed` is a functional store that depends on the values of observables or other computeds. They `computed` is a functional store that depends on the values of properties or other computeds. They
derive value from observables rather than store value and hence cannot be set directly. derive value from properties rather than store value and hence cannot be set directly.
## Behavior ## Behavior
@ -95,7 +87,7 @@ is set, otherwise it just return the stored result from the last time it compute
```js ```js
const showDialog = computed( const showDialog = computed(
(inVP, shouldShow) => inVP && shouldShow, // computation function (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 ### Subscribe to changes
Call the subscribe method with a callback that will be called when the computed result changes to a 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. it will provide the count of remaining subscriptions.
```js ```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: and the computation function is non-trivial. For example:
```js ```js
const inViewport = observable(false); const inViewport = prop(false);
const shouldShow = observable(false); const shouldShow = prop(false);
const showDialog = computed((inVP, shouldShow) => inVP && shouldShow, [inViewport, shouldShow]); 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(); // 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.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 ### Provide a comparator for complex types
@ -164,25 +156,26 @@ function _intersection(a, b) {
return new Set([...a].filter(x => b.has(x))); return new Set([...a].filter(x => b.has(x)));
} }
const a = observable(new Set([1, 2]), setEquals); const a = prop(new Set([1, 2]), setEquals);
const b = observable(new Set([2, 3]), setEquals); const b = prop(new Set([2, 3]), setEquals);
const intersection = computed(_intersection, [a, b], setEquals); const intersection = computed(_intersection, [a, b], setEquals);
``` ```
# [bundle](./src/bundle.js) # [bundle](./src/bundle.js)
`bundle` is a wrapper around a group of `observables` for the purpose of applying changes to all of `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 observable in the them at once without having to trigger a subscription that may depend on more than property in the
group. group.
Another way to think of a `bundle` is an `observable` that takes an object and exposes the Another way to think of a `bundle` is a `property` that takes an object and exposes the object's
properties as individual observables. properties as individual `property` instances.
## Behavior ## Behavior
A `bundle` wraps observables to intercept dependency hooks in such a way that updating all A `bundle` wraps properties to intercept dependency hooks in such a way that updating all `property`
observables can happen at once before any downstream `computeds` are evaluated. A bundle returns a instances can happen at once before any downstream `computed` instances are evaluated. A bundle
function that can be called with an object to set values for the mapped member observables. returns a function that can be called with an object to set values for the mapped member `property`
instances.
## Usage ## Usage
@ -190,14 +183,14 @@ function that can be called with an object to set values for the mapped member o
```js ```js
const layoutEventBundle = bundle({ const layoutEventBundle = bundle({
width: observable(1), width: prop(1),
height: observable(2) height: prop(2)
}); });
const ratio = computed((a, b) => a / b, [layoutEventBundle.width, layoutEventBundle.height]); const ratio = computed((a, b) => a / b, [layoutEventBundle.width, layoutEventBundle.height]);
ratio.subscribe(render); ratio.subscribe(render);
``` ```
### Change Member Observables atomically ### Change Member Properties atomically
```js ```js
layoutEventBundle({ width: 640, height: 480 }); 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` change. But bundle allows both values to change and `ratio` will only be evaluated once and `render`
called once. called once.
### Change Member Observables individually ### Change Member Properties individually
```js ```js
layoutEventBundle.width(640); layoutEventBundle.width(640);
layoutEventBundle.height(480); 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", "name": "frptools",
"version": "1.2.0", "version": "2.0.0",
"description": "Observable and Computed data streams", "description": "Observable Property and Computed data streams",
"main": "lib/index.js", "main": "lib/index.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/index.js",
"files": ["dist", "lib", "src"], "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', () => { describe('bundle', () => {
const methods = { const methods = {
@ -13,10 +13,10 @@ describe('bundle', () => {
spyOn(methods, 'getVal').and.callThrough(); spyOn(methods, 'getVal').and.callThrough();
}); });
it('bundles observable changes together', () => { it('bundles property changes together', () => {
const a = bundle({ const a = bundle({
a: observable(0), a: prop(0),
b: observable(10) b: prop(10)
}); });
const b = computed(methods.square, [a.a]); const b = computed(methods.square, [a.a]);
const c = computed(methods.add, [a.a, a.b]); const c = computed(methods.add, [a.a, a.b]);
@ -47,8 +47,8 @@ describe('bundle', () => {
}); });
it('unbundled changes are less efficient', () => { it('unbundled changes are less efficient', () => {
const a = observable(0); const a = prop(0);
const _b = observable(10); const _b = prop(10);
const b = computed(methods.square, [a]); const b = computed(methods.square, [a]);
const c = computed(methods.add, [a, _b]); const c = computed(methods.add, [a, _b]);
@ -80,8 +80,8 @@ describe('bundle', () => {
it('allows individual members to be updated', () => { it('allows individual members to be updated', () => {
const a = bundle({ const a = bundle({
a: observable(0), a: prop(0),
b: observable(10) b: prop(10)
}); });
const b = computed(methods.square, [a.a]); const b = computed(methods.square, [a.a]);
const c = computed(methods.add, [a.a, a.b]); 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', () => { describe('computed', () => {
const add = (a, b) => a + b; const add = (a, b) => a + b;
const square = a => a * a; const square = a => a * a;
it('returns the value computed from its dependencies', () => { it('returns the value computed from its dependencies', () => {
const a = observable(0); const a = prop(0);
const b = computed(square, [a]); const b = computed(square, [a]);
const c = computed(add, [a, b]); const c = computed(add, [a, b]);
@ -28,7 +28,7 @@ describe('computed', () => {
it('only computes when called', () => { it('only computes when called', () => {
let runCount = 0; let runCount = 0;
let currentValue = 1; let currentValue = 1;
const a = observable(0); const a = prop(0);
const b = computed( const b = computed(
val => { val => {
runCount += 1; runCount += 1;
@ -59,7 +59,7 @@ describe('computed', () => {
let runCount = 0; let runCount = 0;
let subRunCount = 0; let subRunCount = 0;
let currentValue = 1; let currentValue = 1;
const a = observable(0); const a = prop(0);
const b = computed( const b = computed(
val => { val => {
runCount += 1; runCount += 1;
@ -99,7 +99,7 @@ describe('computed', () => {
let runCount = 0; let runCount = 0;
let subRunCount = 0; let subRunCount = 0;
let currentValue = 1; let currentValue = 1;
const a = observable(0); const a = prop(0);
const b = computed( const b = computed(
val => { val => {
runCount += 1; runCount += 1;
@ -142,7 +142,7 @@ describe('computed', () => {
}); });
it('can be detached', () => { it('can be detached', () => {
const a = observable(2); const a = prop(2);
const b = computed(square, [a]); const b = computed(square, [a]);
const c = computed(add, [a, b]); const c = computed(add, [a, b]);
@ -173,8 +173,8 @@ describe('computed', () => {
return new Set([...a].filter(x => b.has(x))); return new Set([...a].filter(x => b.has(x)));
} }
const a = observable(new Set([1, 2]), setEquals); const a = prop(new Set([1, 2]), setEquals);
const b = observable(new Set([2, 3]), setEquals); const b = prop(new Set([2, 3]), setEquals);
const ABintersection = computed(intersection, [a, b], setEquals); const ABintersection = computed(intersection, [a, b], setEquals);
expect(runCount).toEqual(0); 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', () => { it('returns its initialized value', () => {
const a = observable(true); const a = prop(true);
expect(a()).toEqual(true); expect(a()).toEqual(true);
}); });
it('returns its set value', () => { it('returns its set value', () => {
const a = observable(); const a = prop();
expect(a()).toEqual(undefined); expect(a()).toEqual(undefined);
expect(a(true)).toEqual(true); expect(a(true)).toEqual(true);
}); });
@ -15,7 +15,7 @@ describe('observable', () => {
it('returns notifies dependents of updates', () => { it('returns notifies dependents of updates', () => {
let runCount = 0; let runCount = 0;
let currentValue = 1; let currentValue = 1;
const a = observable(); const a = prop();
a.subscribe(val => { a.subscribe(val => {
runCount += 1; runCount += 1;
expect(val).toEqual(currentValue); expect(val).toEqual(currentValue);
@ -39,7 +39,7 @@ describe('observable', () => {
it('honors cancelled subscriptions', () => { it('honors cancelled subscriptions', () => {
let runCount = 0; let runCount = 0;
let currentValue = 1; let currentValue = 1;
const a = observable(); const a = prop();
const cancelSubscription = a.subscribe(val => { const cancelSubscription = a.subscribe(val => {
runCount += 1; runCount += 1;
expect(val).toEqual(currentValue); expect(val).toEqual(currentValue);
@ -74,7 +74,7 @@ describe('observable', () => {
let runCount = 0; let runCount = 0;
const a = observable(new Set([1, 2]), setEquals); const a = prop(new Set([1, 2]), setEquals);
a.subscribe(() => (runCount += 1)); a.subscribe(() => (runCount += 1));
expect([...a()]).toEqual([1, 2]); expect([...a()]).toEqual([1, 2]);
expect(runCount).toEqual(0); expect(runCount).toEqual(0);

View File

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

View File

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

View File

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