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