Add frptools

This commit is contained in:
Timothy Farrell 2017-01-18 08:26:11 -06:00
commit 4e70d07f71
7 changed files with 298 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
lib

37
package.json Normal file
View 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
View 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
View 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
View 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);
});
});

View 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
View 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();