diff --git a/README.md b/README.md index 9bbe686..080b250 100644 --- a/README.md +++ b/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. diff --git a/package.json b/package.json index d527a1a..1b9f4de 100644 --- a/package.json +++ b/package.json @@ -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"], diff --git a/spec/bundle.spec.js b/spec/bundle.spec.js index db7d39e..12a5a13 100644 --- a/spec/bundle.spec.js +++ b/spec/bundle.spec.js @@ -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]); diff --git a/spec/computed.spec.js b/spec/computed.spec.js index d6ff30e..cf6cb50 100644 --- a/spec/computed.spec.js +++ b/spec/computed.spec.js @@ -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); diff --git a/spec/observable.spec.js b/spec/property.spec.js similarity index 89% rename from spec/observable.spec.js rename to spec/property.spec.js index fa1c8cc..05a49e0 100644 --- a/spec/observable.spec.js +++ b/spec/property.spec.js @@ -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); diff --git a/src/bundle.js b/src/bundle.js index 3169467..6e482dc 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -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); diff --git a/src/index.js b/src/index.js index 089f3ba..666042a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,3 @@ -export { observable } from './observable'; +export { prop } from './property'; export { computed } from './computed'; export { bundle } from './bundle'; diff --git a/src/observable.js b/src/property.js similarity index 80% rename from src/observable.js rename to src/property.js index 6c6180f..c3719c3 100644 --- a/src/observable.js +++ b/src/property.js @@ -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));