Add new "container" type to frptools

This commit is contained in:
Timothy Farrell 2017-12-05 21:48:08 -07:00
parent 1e29d2d7a4
commit 17ff59dd0b
6 changed files with 152 additions and 2 deletions

View File

@ -212,3 +212,33 @@ layoutEventBundle.height(480);
``` ```
The properties 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.
# [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.

View File

@ -1,6 +1,6 @@
{ {
"name": "frptools", "name": "frptools",
"version": "2.1.0", "version": "2.2.0",
"description": "Observable Property 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",

61
spec/container.spec.js Normal file
View File

@ -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);
});
});

View File

@ -67,4 +67,4 @@ export function computed(fn, dependencies = [], hash = id) {
return accessor; return accessor;
} }
const runParam = a => a(); const runParam = a => (typeof a === 'function' ? a() : a);

58
src/container.js Normal file
View File

@ -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;
}

View File

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