Rename observable to prop(erty) to distinguish from TC39
This commit is contained in:
parent
8f73b59339
commit
3b84cacbaa
@ -6,7 +6,7 @@ These are my personal projects.
|
||||
|
||||
## [FRP tools](./packages/frptools/README.md)
|
||||
|
||||
Observable and computed value stores designed to work together for storing real state and derived
|
||||
Observable property and computed value stores designed to work together for storing real and derived
|
||||
state.
|
||||
|
||||
## [Portal](./packages/portal/README.md)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export { observable } from './observable';
|
||||
export { prop } from './property';
|
||||
export { computed } from './computed';
|
||||
export { bundle } from './bundle';
|
||||
|
||||
@ -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));
|
||||
@ -16,7 +16,7 @@
|
||||
"domvm": "~3.2.1",
|
||||
"exif-parser": "~0.1.9",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"frptools": "1.2.0",
|
||||
"frptools": "2.0.0",
|
||||
"pica": "~2.0.8",
|
||||
"pouchdb-adapter-http": "~6.3.4",
|
||||
"pouchdb-adapter-idb": "~6.3.4",
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { defineView, defineElement as el } from 'domvm';
|
||||
import { observable, computed } from 'frptools';
|
||||
import { prop, computed } from 'frptools';
|
||||
|
||||
import * as image from '../data/image.js';
|
||||
|
||||
export function ImageView(vm, model) {
|
||||
const { addTag } = model;
|
||||
const imageData = observable(null);
|
||||
const imageData = prop(null);
|
||||
let imageId = null;
|
||||
|
||||
function onAddTag(image_id) {
|
||||
|
||||
@ -2,7 +2,7 @@ import { extractID } from './conversion.js';
|
||||
import { equals } from './set.js';
|
||||
|
||||
export function pouchDocArrayComparator(a, b) {
|
||||
if (!Array.isArray(b)) {
|
||||
if (!Array.isArray(a)) {
|
||||
return false;
|
||||
}
|
||||
const aIDs = a.map(extractID);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { observable, computed } from 'frptools';
|
||||
import { prop, computed } from 'frptools';
|
||||
import { matchesSelector } from 'pouchdb-selector-core';
|
||||
|
||||
import { getDatabase } from '../services/db.js';
|
||||
@ -54,8 +54,8 @@ export function LiveArray(db, selector, watcher) {
|
||||
const _watcher = watcher || Watcher(db, selector);
|
||||
let changeSub = null;
|
||||
|
||||
const ready = observable(false);
|
||||
const data = observable({ docs: [] });
|
||||
const ready = prop(false);
|
||||
const data = prop({ docs: [] });
|
||||
const docs = computed(r => r.docs, [data], pouchDocArrayComparator);
|
||||
|
||||
const idSet = () => docs().reduce((acc, d) => acc.add(d._id), new Set());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user