Add frptools
This commit is contained in:
commit
4e70d07f71
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
lib
|
||||||
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "frptools",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Observable and Computed data streams",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"jsnext:main": "src/index.js",
|
||||||
|
"files": ["dist", "lib", "src"],
|
||||||
|
"keywords": ["reactive"],
|
||||||
|
"author": "Timothy Farrell <tim@thecookiejar.me> (https://github.com/explorigin)",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "6.18.0",
|
||||||
|
"babel-core": "6.21.0",
|
||||||
|
"babel-preset-es2015": "6.18.0",
|
||||||
|
"babel-preset-es2015-rollup": "3.0.0",
|
||||||
|
"babel-preset-stage-0": "6.16.0",
|
||||||
|
"eslint": "3.12.2",
|
||||||
|
"eslint-plugin-flowtype": "2.29.1",
|
||||||
|
"jasmine": "^2.5.3",
|
||||||
|
"rimraf": "2.5.4",
|
||||||
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
|
"rollup-plugin-json": "2.1.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf dist lib",
|
||||||
|
"build:lib": "NODE_ENV=production babel src --presets=\"stage-0,es2015\" --out-dir lib",
|
||||||
|
"build:umd": "npm run build:lib && NODE_ENV=production rollup -c",
|
||||||
|
"build:umd:min":
|
||||||
|
"npm run build:umd && uglifyjs -m --screw-ie8 -c -o dist/frptools.min.js dist/frptools.js",
|
||||||
|
"build:umd:gzip":
|
||||||
|
"npm run build:umd:min && gzip -c9 dist/frptools.min.js > dist/frptools.min.js.gz",
|
||||||
|
"build": "npm run build:umd:gzip && ls -l dist/",
|
||||||
|
"prepublish": "npm run clean && npm run build",
|
||||||
|
"test": "npm run build:lib && jasmine",
|
||||||
|
"uglifyjs": "2.4.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
rollup.config.js
Normal file
19
rollup.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import json from 'rollup-plugin-json';
|
||||||
|
import babel from 'rollup-plugin-babel';
|
||||||
|
|
||||||
|
const babelConfig = {
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
browser: true
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
presets: ['es2015-rollup']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
entry: 'src/index.js',
|
||||||
|
format: 'umd',
|
||||||
|
moduleName: 'frptools',
|
||||||
|
plugins: [json(), babel(babelConfig)],
|
||||||
|
dest: 'dist/frptools.js'
|
||||||
|
};
|
||||||
68
spec/computed.spec.js
Normal file
68
spec/computed.spec.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
const { observable, 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 b = computed(square, [a]);
|
||||||
|
const c = computed(add, [a, b]);
|
||||||
|
|
||||||
|
expect(b()).toEqual(0);
|
||||||
|
expect(c()).toEqual(0);
|
||||||
|
|
||||||
|
a(1);
|
||||||
|
expect(b()).toEqual(1);
|
||||||
|
expect(c()).toEqual(2);
|
||||||
|
|
||||||
|
a(2);
|
||||||
|
expect(b()).toEqual(4);
|
||||||
|
expect(c()).toEqual(6);
|
||||||
|
|
||||||
|
a(3);
|
||||||
|
expect(b()).toEqual(9);
|
||||||
|
expect(c()).toEqual(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only computes when called', () => {
|
||||||
|
let runCount = 0;
|
||||||
|
let currentValue = 1;
|
||||||
|
const a = observable(0);
|
||||||
|
const b = computed(
|
||||||
|
val => {
|
||||||
|
runCount += 1;
|
||||||
|
expect(val).toEqual(currentValue);
|
||||||
|
return val * val;
|
||||||
|
},
|
||||||
|
[a]
|
||||||
|
);
|
||||||
|
|
||||||
|
a(1);
|
||||||
|
expect(runCount).toEqual(0);
|
||||||
|
expect(b()).toEqual(1);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
expect(b()).toEqual(1);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
currentValue = 3;
|
||||||
|
a(3);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
expect(b()).toEqual(9);
|
||||||
|
expect(runCount).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be detached', () => {
|
||||||
|
const a = observable(2);
|
||||||
|
const b = computed(square, [a]);
|
||||||
|
const c = computed(add, [a, b]);
|
||||||
|
|
||||||
|
expect(b()).toEqual(4);
|
||||||
|
expect(c()).toEqual(6);
|
||||||
|
|
||||||
|
b.detach();
|
||||||
|
|
||||||
|
a(3);
|
||||||
|
expect(b()).toEqual(4);
|
||||||
|
expect(c()).toEqual(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
55
spec/observable.spec.js
Normal file
55
spec/observable.spec.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const { observable } = require('../lib/index.js');
|
||||||
|
|
||||||
|
describe('observable', () => {
|
||||||
|
it('returns its initialized value', () => {
|
||||||
|
const a = observable(true);
|
||||||
|
expect(a()).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns its set value', () => {
|
||||||
|
const a = observable();
|
||||||
|
expect(a()).toEqual(undefined);
|
||||||
|
expect(a(true)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns notifies dependents of updates', () => {
|
||||||
|
let runCount = 0;
|
||||||
|
let currentValue = 1;
|
||||||
|
const a = observable();
|
||||||
|
a.subscribe(val => {
|
||||||
|
runCount += 1;
|
||||||
|
expect(val).toEqual(currentValue);
|
||||||
|
});
|
||||||
|
expect(a(1)).toEqual(1);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
expect(a(1)).toEqual(1);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
currentValue = 2;
|
||||||
|
expect(a(2)).toEqual(2);
|
||||||
|
expect(runCount).toEqual(2);
|
||||||
|
expect(a(2)).toEqual(2);
|
||||||
|
expect(runCount).toEqual(2);
|
||||||
|
currentValue = 1;
|
||||||
|
expect(a(1)).toEqual(1);
|
||||||
|
expect(runCount).toEqual(3);
|
||||||
|
expect(a(1)).toEqual(1);
|
||||||
|
expect(runCount).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors cancelled subscriptions', () => {
|
||||||
|
let runCount = 0;
|
||||||
|
let currentValue = 1;
|
||||||
|
const a = observable();
|
||||||
|
const cancelSubscription = a.subscribe(val => {
|
||||||
|
runCount += 1;
|
||||||
|
expect(val).toEqual(currentValue);
|
||||||
|
});
|
||||||
|
expect(a(1)).toEqual(1);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
expect(a(1)).toEqual(1);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
cancelSubscription();
|
||||||
|
expect(a(3)).toEqual(3);
|
||||||
|
expect(runCount).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
7
spec/support/jasmine.json
Normal file
7
spec/support/jasmine.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"spec_dir": "spec",
|
||||||
|
"spec_files": ["**/*[sS]pec.js"],
|
||||||
|
"helpers": ["helpers/**/*.js"],
|
||||||
|
"stopSpecOnExpectationFailure": false,
|
||||||
|
"random": false
|
||||||
|
}
|
||||||
109
src/index.js
Normal file
109
src/index.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// 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.
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// Creation:
|
||||||
|
// `const inViewport = observable(true);`
|
||||||
|
// Creates and sets initial value to `true`
|
||||||
|
//
|
||||||
|
// Read:
|
||||||
|
// `if (inViewport()) { }`
|
||||||
|
// Call it to receive the stored value.
|
||||||
|
//
|
||||||
|
// Change:
|
||||||
|
// `inViewport(false);`
|
||||||
|
// Call it passing the new value. If any computed stores depend on this value
|
||||||
|
// they will be marked dirty and re-evaluated the next time they are read from.
|
||||||
|
//
|
||||||
|
// Subscribe to changes:
|
||||||
|
// `inViewport.subscribe(console.log.bind(console))`
|
||||||
|
// Call the subscribe method with a callback that will be called when the
|
||||||
|
// observable is changed to a different value.
|
||||||
|
|
||||||
|
export function observable(store) {
|
||||||
|
const subscribers = new Set();
|
||||||
|
|
||||||
|
const accessor = function _observable(newVal) {
|
||||||
|
if (newVal !== undefined && store !== newVal) {
|
||||||
|
store = newVal;
|
||||||
|
subscribers.forEach(s => s(store));
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
||||||
|
accessor.subscribe = accessor._d = fn => {
|
||||||
|
subscribers.add(fn);
|
||||||
|
return () => subscribers.delete(fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
accessor.unsubscribeAll = () => subscribers.clear();
|
||||||
|
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// computed is a functional store that depends on the values of observables or other computeds. They cannot be set directly.
|
||||||
|
//
|
||||||
|
// Behavior:
|
||||||
|
// computed will subscribe to its dependencies in such a way that it will be marked as "dirty" when any dependency changes.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// Creation:
|
||||||
|
// const showDialog = computed((inVP, shouldShow) => (inVP && shouldShow), [inViewport, shouldShow]);
|
||||||
|
//
|
||||||
|
// Read:
|
||||||
|
// `if (showDialog()) { alert("Hi"); }`
|
||||||
|
// Call it to receive the stored value.
|
||||||
|
|
||||||
|
export function computed(fn, dependencies = []) {
|
||||||
|
const subscribers = new Set();
|
||||||
|
const dependents = new Set();
|
||||||
|
let val = undefined;
|
||||||
|
let isDirty = true;
|
||||||
|
|
||||||
|
function _computedDirtyReporter() {
|
||||||
|
if (!isDirty) {
|
||||||
|
isDirty = true;
|
||||||
|
}
|
||||||
|
dependents.forEach(runParam);
|
||||||
|
|
||||||
|
if (subscribers.size) {
|
||||||
|
accessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependentSubscriptions = Array.from(dependencies).map(d => d._d(_computedDirtyReporter));
|
||||||
|
|
||||||
|
const accessor = function _computed() {
|
||||||
|
if (isDirty) {
|
||||||
|
const newVal = fn.apply(null, dependencies.map(runParam));
|
||||||
|
isDirty = false;
|
||||||
|
if (newVal !== val) {
|
||||||
|
val = newVal;
|
||||||
|
subscribers.forEach(s => s(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
accessor.subscribe = fn => {
|
||||||
|
subscribers.add(fn);
|
||||||
|
return () => subscribers.delete(fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
accessor._d = fn => {
|
||||||
|
dependents.add(fn);
|
||||||
|
return () => dependents.delete(fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
accessor.detach = () => {
|
||||||
|
subscribers.clear();
|
||||||
|
dependents.clear();
|
||||||
|
dependentSubscriptions.forEach(runParam);
|
||||||
|
};
|
||||||
|
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runParam = a => a();
|
||||||
Reference in New Issue
Block a user