reactive-function
Advanced tools
Comparing version 0.2.0 to 0.3.0
85
index.js
@@ -19,3 +19,4 @@ var ReactiveProperty = require("reactive-property"); | ||
// A lookup table for properties based on their assigned id. | ||
// A map for properties based on their assigned id. | ||
// Keys are property ids, values are reactive properties. | ||
var propertiesById = {}; | ||
@@ -34,45 +35,38 @@ | ||
//ReactiveFunction(dependencies... , callback) | ||
function ReactiveFunction(){ | ||
function ReactiveFunction(options){ | ||
// TODO check for wrong number of args | ||
var inputs = options.inputs; | ||
var callback = options.callback; | ||
var output = options.output; | ||
// Parse arguments. | ||
var args = Array.apply(null, arguments); | ||
var dependencies = args.splice(1); | ||
var callback = args[0]; | ||
// TODO check for correct type for inputs | ||
// TODO check for correct type for callback | ||
// This stores the output value. | ||
var value; | ||
// The returned reactive function acts as a getter (not a setter). | ||
var reactiveFunction = function (){ | ||
if(arguments.length > 0){ throw errors.notASetter; } | ||
return value; | ||
}; | ||
var reactiveFunction = {}; | ||
// This gets invoked during a digest, after dependencies have been evaluated. | ||
reactiveFunction.evaluate = function (){ | ||
value = callback.apply(null, dependencies.map(function (dependency){ | ||
return dependency(); | ||
})); | ||
// This gets invoked during a digest, after inputs have been evaluated. | ||
output.evaluate = function (){ | ||
var values = inputs.map(function (input){ | ||
return input(); | ||
}); | ||
if(defined(values)){ | ||
output(callback.apply(null, values)); | ||
} | ||
}; | ||
// Assign node ids to dependencies and the reactive function. | ||
assignId(reactiveFunction); | ||
dependencies.forEach(assignId); | ||
// Assign node ids to inputs and the reactive function. | ||
assignId(output); | ||
inputs.forEach(assignId); | ||
// Set up edges in the graph from each dependency. | ||
dependencies.forEach(function (dependency){ | ||
graph.addEdge(dependency.id, reactiveFunction.id); | ||
// Set up edges in the graph from each input. | ||
inputs.forEach(function (input){ | ||
graph.addEdge(input.id, output.id); | ||
}); | ||
// Compute which of the dependencies are properties (excludes reactive functions). | ||
var properties = dependencies | ||
.filter(function (dependency){ | ||
return dependency.on; | ||
}); | ||
// Add change listeners to each dependency that is a property. | ||
var listeners = properties | ||
// Add change listeners to each input property. | ||
var listeners = inputs | ||
.map(function (property){ | ||
@@ -89,12 +83,10 @@ return property.on(function (){ | ||
// TODO test | ||
// Remove change listeners from dependencies that are properties. | ||
// Remove change listeners from inputs. | ||
listeners.forEach(function (listener, i){ | ||
properties[i].off(listener); | ||
inputs[i].off(listener); | ||
}); | ||
// TODO test | ||
// Remove the edges that were added to the dependency graph. | ||
dependencies.forEach(function (dependency){ | ||
graph.removeEdge(dependency.id, reactiveFunction.id); | ||
inputs.forEach(function (input){ | ||
graph.removeEdge(input.id, output.id); | ||
}); | ||
@@ -109,2 +101,3 @@ | ||
ReactiveFunction.digest = function (){ | ||
graph | ||
@@ -115,5 +108,6 @@ .topologicalSort(Object.keys(changedNodes)) | ||
}) | ||
.forEach(function (reactiveFunction){ | ||
reactiveFunction.evaluate(); | ||
.forEach(function (property){ | ||
property.evaluate(); | ||
}); | ||
changedNodes = {}; | ||
@@ -138,2 +132,9 @@ }; | ||
// Returns true if all elements of the given array are defined. | ||
function defined(arr){ | ||
return !arr.some(function (d){ | ||
return typeof d === "undefined" || d === null; | ||
}); | ||
} | ||
module.exports = ReactiveFunction; |
{ | ||
"name": "reactive-function", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "A library for managing data flows and changing state.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
187
README.md
# reactive-function | ||
A library for managing data flows and changing state. | ||
# Usage | ||
If you are using NPM, install this package with: | ||
`npm install reactive-function` | ||
Require it in your code like this: | ||
```javascript | ||
var ReactiveFunction = require("reactive-function"); | ||
``` | ||
This library is designed to work with [reactive-property](https://github.com/curran/reactive-property), you'll need that too. | ||
```javascript | ||
var ReactiveProperty = require("reactive-property"); | ||
``` | ||
Let's say you have two reactive properties to represent someone's name. | ||
```javascript | ||
var firstName = ReactiveProperty("Jane"); | ||
var lastName = ReactiveProperty("Smith"); | ||
``` | ||
You can define a reactive function that depends on those two properties like this. | ||
```javascript | ||
var fullName = ReactiveFunction(function (first, last){ | ||
return first + " " + last; | ||
}, firstName, lastName); | ||
``` | ||
This defines a "reactive function" that will be invoked when its dependencies (`firstName` and `lastName`) are both defined and whenever either one changes. The function will be invoked on the next tick of the JavaScript event loop after it is defined and after any dependencies change. | ||
To force a synchronous evaluation of all reactive functions whose dependencies have updated, you can call: | ||
```javascript | ||
ReactiveFunction.digest(); | ||
``` | ||
Now you can access the computed value of the reactive function by invoking it. | ||
```javascript | ||
console.log(fullName()); // Prints "Jane Smith" | ||
``` | ||
Here's some sample code from the tests that demonstrates the features of this library. | ||
```javascript | ||
it("Should depend on any number of reactive properties.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveProperty(15); | ||
var d = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
var e = ReactiveFunction(function (a, b, c){ | ||
return a + b + c; | ||
}, a, b, c); | ||
ReactiveFunction.digest(); | ||
assert.equal(d(), 10); | ||
assert.equal(e(), 30); | ||
}); | ||
it("Should depend on a reactive function.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
var c = ReactiveFunction(function (b){ | ||
return b / 2; | ||
}, b); | ||
ReactiveFunction.digest(); | ||
assert.equal(c(), 5); | ||
}); | ||
it("Should depend on a reactive property and a reactive function.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
var d = ReactiveFunction(function (b, c){ | ||
return b + c; | ||
}, b, c); | ||
ReactiveFunction.digest(); | ||
assert.equal(d(), 20); | ||
}); | ||
it("Should handle tricky case.", function () { | ||
// a | ||
// / \ | ||
// b | | ||
// | d | ||
// c | | ||
// \ / | ||
// e | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveFunction(function (a){ return a * 2; }, a); | ||
var c = ReactiveFunction(function (b){ return b + 5; }, b); | ||
var d = ReactiveFunction(function (a){ return a * 3; }, a); | ||
var e = ReactiveFunction(function (c, d){ return c + d; }, c, d); | ||
ReactiveFunction.digest(); | ||
assert.equal(e(), ((a() * 2) + 5) + (a() * 3)); | ||
}); | ||
it("Should throw an error if attempting to set the value directly.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
assert.throws(function (){ | ||
b(5); | ||
}); | ||
}); | ||
it("Should clear changed nodes on digest.", function () { | ||
var numInvocations = 0; | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveFunction(function (a){ | ||
numInvocations++; | ||
return a * 2; | ||
}, a); | ||
ReactiveFunction.digest(); | ||
ReactiveFunction.digest(); | ||
assert.equal(numInvocations, 1); | ||
}); | ||
it("Should automatically digest on next tick.", function (done) { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveFunction(function (a, b){ | ||
return a + b; | ||
}, a, b); | ||
setTimeout(function (){ | ||
assert.equal(c(), 15); | ||
done(); | ||
}, 0); | ||
}); | ||
``` | ||
# Case Study: Width and Height | ||
Suppose you have some reactive properties that change, such as `width` and `height`. | ||
```javascript | ||
var my = { | ||
width: ReactiveProperty(), | ||
height: ReactiveProperty() | ||
}; | ||
``` | ||
Suppose also that you have a function that resizes an SVG element that depends on those two properties. | ||
```javascript | ||
var svg = d3.select("body").append("svg"); | ||
function render(){ | ||
svg | ||
.attr("width", my.width()) | ||
.attr("height", my.height()); | ||
} | ||
``` | ||
How can you invoke the `render function` when `my.width` and `my.height` change? | ||
Related work: | ||
* [ReactiveJS](https://github.com/mattbaker/Reactive.js) |
247
test.js
@@ -13,9 +13,16 @@ // Unit tests for reactive-function. | ||
it("Should depend on two reactive properties.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveFunction(function (a, b){ | ||
return a + b; | ||
}, a, b); | ||
var c = ReactiveProperty(); | ||
ReactiveFunction({ | ||
inputs: [a, b], | ||
output: c, | ||
callback: function (a, b){ | ||
return a + b; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
@@ -30,10 +37,21 @@ assert.equal(c(), 15); | ||
var d = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
var d = ReactiveProperty(); | ||
var e = ReactiveProperty(); | ||
var e = ReactiveFunction(function (a, b, c){ | ||
return a + b + c; | ||
}, a, b, c); | ||
ReactiveFunction({ | ||
inputs: [a], | ||
output: d, | ||
callback: function (a){ | ||
return a * 2; | ||
} | ||
}); | ||
ReactiveFunction({ | ||
inputs: [a, b, c], | ||
output: e, | ||
callback: function (a, b, c){ | ||
return a + b + c; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
@@ -45,13 +63,25 @@ | ||
it("Should depend on a reactive function.", function () { | ||
it("Should depend on a reactive function output.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
var b = ReactiveProperty(); | ||
var c = ReactiveProperty(); | ||
var c = ReactiveFunction(function (b){ | ||
return b / 2; | ||
}, b); | ||
ReactiveFunction({ | ||
inputs: [a], | ||
output: b, | ||
callback: function (a){ | ||
return a * 2; | ||
} | ||
}); | ||
ReactiveFunction({ | ||
inputs: [b], | ||
output: c, | ||
callback: function (b){ | ||
return b / 2; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
@@ -61,14 +91,26 @@ assert.equal(c(), 5); | ||
it("Should depend on a reactive property and a reactive function.", function () { | ||
it("Should depend on a property and a reactive function output.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
var c = ReactiveProperty(); | ||
var d = ReactiveProperty(); | ||
var d = ReactiveFunction(function (b, c){ | ||
return b + c; | ||
}, b, c); | ||
ReactiveFunction({ | ||
inputs: [a], | ||
output: c, | ||
callback: function (a){ | ||
return a * 2; | ||
} | ||
}); | ||
ReactiveFunction({ | ||
inputs: [b, c], | ||
output: d, | ||
callback: function (b, c){ | ||
return b + c; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
@@ -88,7 +130,12 @@ assert.equal(d(), 20); | ||
var b = ReactiveFunction(function (a){ return a * 2; }, a); | ||
var c = ReactiveFunction(function (b){ return b + 5; }, b); | ||
var d = ReactiveFunction(function (a){ return a * 3; }, a); | ||
var e = ReactiveFunction(function (c, d){ return c + d; }, c, d); | ||
var b = ReactiveProperty(); | ||
var c = ReactiveProperty(); | ||
var d = ReactiveProperty(); | ||
var e = ReactiveProperty(); | ||
ReactiveFunction({ inputs: [a], output: b, callback: function (a){ return a * 2; } }); | ||
ReactiveFunction({ inputs: [b], output: c, callback: function (b){ return b + 5; } }); | ||
ReactiveFunction({ inputs: [a], output: d, callback: function (a){ return a * 3; } }); | ||
ReactiveFunction({ inputs: [c, d], output: e, callback: function (c, d){ return c + d; } }); | ||
ReactiveFunction.digest(); | ||
@@ -99,13 +146,20 @@ | ||
it("Should throw an error if attempting to set the value directly.", function () { | ||
var a = ReactiveProperty(5); | ||
// Not sure if this is really necessary. | ||
// It would be a nice safeguard but would clutter the code. | ||
//it("Should throw an error if attempting to set the value directly.", function () { | ||
// var a = ReactiveProperty(5); | ||
// var b = ReactiveProperty(); | ||
var b = ReactiveFunction(function (a){ | ||
return a * 2; | ||
}, a); | ||
// ReactiveFunction({ | ||
// inputs: [a], | ||
// output: b, | ||
// callback: function (a){ | ||
// return a * 2; | ||
// } | ||
// }); | ||
assert.throws(function (){ | ||
b(5); | ||
}); | ||
}); | ||
// assert.throws(function (){ | ||
// b(5); | ||
// }); | ||
//}); | ||
@@ -115,6 +169,13 @@ it("Should clear changed nodes on digest.", function () { | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveFunction(function (a){ | ||
numInvocations++; | ||
return a * 2; | ||
}, a); | ||
var b = ReactiveProperty(); | ||
ReactiveFunction({ | ||
inputs: [a], | ||
output: b, | ||
callback: function (a){ | ||
numInvocations++; | ||
return a * 2; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
@@ -128,6 +189,11 @@ ReactiveFunction.digest(); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveProperty(); | ||
var c = ReactiveFunction(function (a, b){ | ||
return a + b; | ||
}, a, b); | ||
ReactiveFunction({ | ||
inputs: [a, b], | ||
output: c, | ||
callback: function (a, b){ | ||
return a + b; | ||
} | ||
}); | ||
@@ -139,2 +205,97 @@ setTimeout(function (){ | ||
}); | ||
it("Should remove listeners on destroy.", function (done){ | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveProperty(); | ||
var rf = ReactiveFunction({ | ||
inputs: [a, b], | ||
output: c, | ||
callback: function (a, b){ | ||
return a + b; | ||
} | ||
}); | ||
// Wait until after the first auto-digest. | ||
setTimeout(function (){ | ||
rf.destroy(); | ||
// Without the call to destroy(), this would trigger a digest. | ||
a(20); | ||
// Give time for an auto-digest to occur. | ||
setTimeout(function (){ | ||
// Confirm that a digest did not occur. | ||
assert.equal(c(), 15); | ||
done(); | ||
}, 0); | ||
}, 0); | ||
}); | ||
it("Should remove edges on destroy.", function (){ | ||
var a = ReactiveProperty(5); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveProperty(); | ||
var rf = ReactiveFunction({ | ||
inputs: [a, b], | ||
output: c, | ||
callback: function (a, b){ | ||
return a + b; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
assert.equal(c(), 15); | ||
rf.destroy(); | ||
a(20); | ||
ReactiveFunction.digest(); | ||
assert.equal(c(), 15); | ||
}); | ||
it("Should not invoke if an input is undefined.", function (){ | ||
var numInvocations = 0; | ||
var a = ReactiveProperty(); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveProperty(); | ||
ReactiveFunction({ | ||
inputs: [a, b], | ||
output: c, | ||
callback: function (a, b){ | ||
numInvocations++; | ||
return a + b; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
assert.equal(numInvocations, 0); | ||
}); | ||
it("Should not invoke if an input is null.", function (){ | ||
var numInvocations = 0; | ||
var a = ReactiveProperty(null); | ||
var b = ReactiveProperty(10); | ||
var c = ReactiveProperty(); | ||
ReactiveFunction({ | ||
inputs: [a, b], | ||
output: c, | ||
callback: function (a, b){ | ||
numInvocations++; | ||
return a + b; | ||
} | ||
}); | ||
ReactiveFunction.digest(); | ||
assert.equal(numInvocations, 0); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
16540
336
190
1