From 08e141b46813a84eea0008caaa3cae10cf7c9993 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Tue, 5 Dec 2017 21:48:08 -0700 Subject: [PATCH] Add new "container" type to frptools --- packages/frptools/README.md | 30 ++++++++++++ packages/frptools/package.json | 2 +- packages/frptools/spec/container.spec.js | 61 ++++++++++++++++++++++++ packages/frptools/src/computed.js | 2 +- packages/frptools/src/container.js | 58 ++++++++++++++++++++++ packages/frptools/src/index.js | 1 + 6 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 packages/frptools/spec/container.spec.js create mode 100644 packages/frptools/src/container.js diff --git a/packages/frptools/README.md b/packages/frptools/README.md index e750ee6..515557c 100644 --- a/packages/frptools/README.md +++ b/packages/frptools/README.md @@ -212,3 +212,33 @@ layoutEventBundle.height(480); ``` The properties exposed by the bundle can also be updated apart from their grouping. + +# [container](./src/container.js) + +`container` is a wrapper around any container type (object, Set, Map, or Array) while monitoring +changes to the container. A container can be subscribed to and `computed` instances can depend on +them. + +## Behavior + +Anytime a property is set or a method is gotten and called, the container will check for an updated +state and trigger subscribers if it is updated. An hash function must be applied to determine +updated status otherwise subscribers will be called on any potential update. + +## Usage + +### Creation + +```js +const monkeys = contained([], arr => arr.join('$')); +const firstMonkey = computed(m => (m.length ? m[0] : null), [monkeys]); +firstMonkey.subscribe(console.log.bind.console); +``` + +### Add a member to the container + +```js +monkeys.push('Bill'); +``` + +_firstMonkey_ would be computed and "Bill" would be logged to the console. diff --git a/packages/frptools/package.json b/packages/frptools/package.json index f71a7ca..954595b 100644 --- a/packages/frptools/package.json +++ b/packages/frptools/package.json @@ -1,6 +1,6 @@ { "name": "frptools", - "version": "2.1.0", + "version": "2.2.0", "description": "Observable Property and Computed data streams", "main": "lib/index.js", "jsnext:main": "src/index.js", diff --git a/packages/frptools/spec/container.spec.js b/packages/frptools/spec/container.spec.js new file mode 100644 index 0000000..31d7ef4 --- /dev/null +++ b/packages/frptools/spec/container.spec.js @@ -0,0 +1,61 @@ +const { container, computed } = require('../lib/index.js'); +const { hashSet } = require('../lib/util.js'); + +describe('A container', () => { + it('notifies dependents of updates', () => { + let runCount = 0; + let currentValue = new Set(); + const a = container(new Set(), hashSet); + const b = computed(s => Array.from(s).reduce((i, acc) => i + acc, 0), [a]); + a.subscribe(val => { + runCount += 1; + expect(hashSet(a)).toEqual(hashSet(currentValue)); + }); + currentValue.add(1); + a.add(1); + expect(runCount).toEqual(1); + expect(b()).toEqual(1); + currentValue.add(2); + a.add(2); + expect(runCount).toEqual(2); + expect(b()).toEqual(3); + }); + + it('works for arrays', () => { + let runCount = 0; + let currentValue = []; + const a = container([], arr => arr.join('x')); + a.subscribe(val => { + runCount += 1; + expect(a.join('x')).toEqual(currentValue.join('x')); + }); + currentValue.push(1); + a.push(1); + expect(runCount).toEqual(1); + currentValue.push(2); + a.push(2); + expect(runCount).toEqual(2); + currentValue.push(3); + a._.push(3); + expect(runCount).toEqual(2); + }); + + it('._ returns the proxied element', () => { + let runCount = 0; + let currentValue = new Set(); + const a = container(new Set(), hashSet); + a.subscribe(val => { + runCount += 1; + expect(hashSet(a)).toEqual(hashSet(currentValue)); + }); + currentValue.add(1); + a.add(1); + expect(runCount).toEqual(1); + currentValue.add(2); + a.add(2); + expect(runCount).toEqual(2); + currentValue.add(3); + a._.add(3); + expect(runCount).toEqual(2); + }); +}); diff --git a/packages/frptools/src/computed.js b/packages/frptools/src/computed.js index 40e25a9..bd808b6 100644 --- a/packages/frptools/src/computed.js +++ b/packages/frptools/src/computed.js @@ -67,4 +67,4 @@ export function computed(fn, dependencies = [], hash = id) { return accessor; } -const runParam = a => a(); +const runParam = a => (typeof a === 'function' ? a() : a); diff --git a/packages/frptools/src/container.js b/packages/frptools/src/container.js new file mode 100644 index 0000000..7d5eef3 --- /dev/null +++ b/packages/frptools/src/container.js @@ -0,0 +1,58 @@ +export function container(store, hash) { + const subscribers = new Set(); + let id = hash(store); + + const containerMethods = { + subscribe: fn => { + subscribers.add(fn); + return () => { + subscribers.delete(fn); + return subscribers.size; + }; + }, + unsubscribeAll: () => subscribers.clear() + }; + containerMethods._d = containerMethods.subscribe; + + function checkUpdate() { + const newId = hash(store); + if (id !== newId) { + id = newId; + subscribers.forEach(s => s(store)); + } + } + + const p = new Proxy(store, { + apply: (target, context, args) => { + return target; + }, + get: (target, name) => { + if (name in containerMethods) { + return containerMethods[name]; + } + if (name === '_') { + return target; + } + const thing = target[name]; + if (typeof thing === 'function') { + return (...args) => { + const ret = target[name](...args); + checkUpdate(); + return ret; + }; + } + return thing; + }, + set: (target, name, newVal) => { + if (name in containerMethods) { + throw new ReferenceError(`Cannot set ${name} in ${target}`); + } + target[name] = newVal; + checkUpdate(); + + return newVal; + } + }); + + return p; +} diff --git a/packages/frptools/src/index.js b/packages/frptools/src/index.js index 666042a..30681ef 100644 --- a/packages/frptools/src/index.js +++ b/packages/frptools/src/index.js @@ -1,3 +1,4 @@ export { prop } from './property'; export { computed } from './computed'; export { bundle } from './bundle'; +export { container } from './container';