reactive-model
v0.13.0
Published
A library for data flow components.
Downloads
49
Readme
reactive-model
A JavaScript library for dataflow programming.
This library provides an abstraction for reactive data flows. This means you can define so-called "reactive functions" in terms of their inputs and output, and the library will take care of executing these functions in the correct order. When input properties change, those changes are propagated through the data flow graph based on topological sorting.
Table of Contents
Installing
You can include reactive-model in your HTML like this (will introduce a global variable ReactiveModel
):
<script src="//datavis-tech.github.io/reactive-model/reactive-model-v0.12.0.min.js"></script>
If you are using NPM, install with npm install reactive-model
, then require the module in your code like this:
var ReactiveModel = require("reactive-model");
Examples
Bl.ocks
ABCs
AB
Here is an example where b
gets set to a + 1
whenever a
changes:
var my = ReactiveModel()
("a") // Create the property "a" with no default value.
("b", function (a){
return a + 1;
}, "a");
The naming convention of my
pays homage to Towards Reusable Charts.
ABC
Here's an example that assign b = a + 1
and c = b + 1
.
function increment(x){ return x + 1; }
var my = ReactiveModel()
("a", 5) // Create the property "a" with a default value of 5.
("b", increment, "a")
("c", increment, "b");
See also ABC in reactive-function.
CDE
Here's an example that shows a reactive function with two inputs, where e = c + d
.
function add(x, y){ return x + y; }
var my = ReactiveModel()
("c", 5)
("d", 10)
("e", add, ["c", "d"]);
Full Name
Consider a Web application that greets a user. The user can enter his or her first name and last name, and the application will display a greeting using their full name. To start with, we can construct a ReactiveModel instance and add properties firstName
and lastName
(with no default values).
var my = ReactiveModel()
("firstName")
("lastName");
After properties are added, they are exposed as chainable getter-setters on my
. Here's how you can set their values.
my.firstName("Jane")
.lastName("Smith");
Next, we set up a reactive function that computes fullName
.
my("fullName", function (firstName, lastName){
return firstName + " " + lastName;
}, "firstName, lastName");
Once we have fullName
defined, we can use it as an input to another reactive function that computes the greeting.
my("greeting", function (fullName){
return "Hello " + fullName + "!";
}, "fullName");
When input properties are defined, the changes will automatically propagate on the next animation frame. If you don't want to wait until the next animation frame for changes to propagate, you can force synchronous propagation by invoking digest.
ReactiveModel.digest();
This ensures that the value of computed properties will be immediately available. We can access them like this.
console.log(my.fullName()); // Prints "Jane Smith"
console.log(my.greeting()); // Prints "Hello Jane Smith!"
Reactive functions that have side effects but no output value can be defined by omitting the output property name argument. This is useful for DOM manipulation, such as passing the greeting text into a DOM element using D3.
my(function (greeting){
d3.select("#greeting").text(greeting);
}, "greeting");
Here's a complete working example that extends the above example code to interact with DOM elements.
Tricky Cases
Tricky Case I
Reactive functions can be combined to create arbitrarily complex data flow graphs. Here's an example that demonstrates why topological sorting is the correct algorithm for computing the order in which to execute reactive functions. In this graph, propagation using breadth-first search (which is what Model.js and some other libraries use) would cause e
to be set twice, and the first time it would be set with an inconsistent state. Using topological sorting for change propagation guarantees that e
will only be set once, and there will never be inconsistent states.
function increment(x){ return x + 1; }
function add(x, y){ return x + y; }
var my = ReactiveModel()
("a", 5)
("b", increment, "a")
("c", increment, "b")
("d", increment, "a")
("e", add, "b, d");
See also Tricky Case in reactive-function.
Tricky Case II
Here's a similar case that reactive-model handles correctly. If breadth-first search were used in this case, then h
would get set 3 times, the first two times with an inconsistent state.
function increment(x){ return x + 1; }
function add3(x, y, z){ return x + y + z; }
var my = ReactiveModel()
("a", 5)
("b", increment, "a")
("c", increment, "b")
("d", increment, "c")
("e", increment, "a")
("f", increment, "e")
("g", increment, "a")
("h", add3, "d, f, g");
For more detailed example code, have a look at the tests.
API Reference
Models
# ReactiveModel()
Constructs a new reactive model instance.
Example:
var model = ReactiveModel();
# model.destroy()
Cleans up resources allocated to this model. Invokes
- reactiveFunction.destroy() on all reactive functions created on this model, and
- reactiveProperty.destroy() on all properties created on this model.
You should invoke this function when finished using model instances in order to avoid memory leaks.
Properties
# model(propertyName[, defaultValue])
Adds a property to the model. Returns the model to support chaining.
Arguments:
- propertyName - The name of the property (a string).
- defaultValue (optional) - The default value for this property.
After a property is added, it is exposed as an instance of reactive-property on the model object at model[propertyName]
.
Example:
var model = ReactiveModel();
// Add property "a" with a default value of 5.
model("a", 5);
// Acces the value of "a".
console.log(model.a()); // Prints 5.
// Set the value of "a".
model.a(10);
// Acces the default value of "a".
console.log(model.a.default()); // Prints 5.
See also reactive-property.
Data Flow
# model([output,] callback, inputs)
Adds a reactive function to this model.
Arguments:
- output (optional) - The output property name.
- callback - The reactive function callback. Arguments are values corresponding to inputs. May be of two forms:
- callback(arguments…) For synchronous reactive functions. The returned value will be assigned to output.
- callback(arguments…, done) For asynchronous reactive functions. The function done should be invoked asynchronously with the value to assign to output. The returned value is ignored.
- inputs - The input property names. May be either
- a comma-delimited list of input property names (e.g.
"a, b"
), or - an array of property name strings (e.g.
["a", "b"]
).
- a comma-delimited list of input property names (e.g.
The callback will be invoked:
- when all input properties are defined,
- after any input properties change,
- during a digest.
An input property is considered "defined" if it has any value other than undefined
(null
is considered defined).
An input property is considered "changed" when
- the reactive function is initially set up, and
- whenever its value is set.
Any input property for one reactive function may also be the output of another.
Here's an example of an asynchronous reactive function.
var model = ReactiveModel()
("a", 50)
("b", function (a, done){
setTimeout(function (){
done(a + 1);
}, 500);
}, "a");
See also ReactiveFunction.
# ReactiveModel.link(propertyA, propertyB)
Sets up one-way data binding from propertyA to propertyB. Returns an instance of ReactiveFunction.
This can be used to set up data flow between two different models. For example, a computed property on one model can be linked to a configurable input property of another model. This function enables model instances to be treated as data flow components, and allows them to be assembled into user-defined data flow graphs.
Arguments:
- propertyA - A reactive-property.
- propertyB - A reactive-property that will be set to the value of propertyA and updated whenever propertyA changes.
Example:
var model1 = ReactiveModel()
("someOutput", 5);
var model2 = ReactiveModel()
("someInput", 10);
var link = ReactiveModel.link(model1.someOutput, model2.someInput);
ReactiveModel.digest();
console.log(model2.someInput()); // Prints 5
model1.someOutput(500);
ReactiveModel.digest();
console.log(model2.someInput()); // Prints 500
// The link needs to be explicitly destroyed, independently from the models.
link.destroy();
This is the same function as ReactiveFunction.link.
# ReactiveModel.digest()
Synchronously evaluates the data flow graph.
This is the same function as ReactiveFunction.digest().
Example:
my
.width(100)
.height(200);
ReactiveModel.digest();
# model.digest()
Synchronously evaluates the data flow graph. Returns the model to support chaining.
This is the same function as ReactiveFunction.digest().
Example:
my
.width(100)
.height(200)
.digest();
# model.call(function[, arguments…])
Invokes the function, passing in model along with any optional arguments. Returns the model to support chaining.
Example:
function fullName(my, first, last) {
my
("firstName", first)
("lastName", last)
("fullName", function (firstName, lastName){
return firstName + " " + lastName;
}, "firstName, lastName");
}
The above function can be invoked like this:
var model = ReactiveModel()
.call(fullName, "Jane", "Smith");
This is equivalent to:
var model = ReactiveModel();
fullName(model, "Jane", "Smith");
Configuration
# model.expose()
Exposes the previously added property to the configuration. Returns the model to support chaining.
The property to expose must have a default value defined.
Here's an example where two properties x
and y
are defined with default values and exposed to the configuration.
var model = new ReactiveModel()
("x", 5).expose()
("y", 6).expose();
# model()
Returns the configuration, an Object where
- keys are property names, and
- values are current property values.
The configuration only contains exposed properties that have values other than their defaults.
Example:
var model = new ReactiveModel()
("x", 5).expose()
("y", 6).expose();
model.x(50);
var configuration = model();
The value of configuration
will be:
{ "x": 50 }
Note that y
is omitted, because it has its default value.
# model(configuration)
Sets the configuration.
The argument configuration is an Object where
- keys are property names, and
- values are property values to be set.
Only exposed properties may be set via the configuration. Exposed properties whose values are not included in configuration will be set to their default values.
Example:
var model = new ReactiveModel()
("x", 5).expose()
("y", 6).expose();
model.x(50);
// Set the configuration.
model({ y: 60 });
console.log(model.x()); // Prints 5 (x was set back to its default value).
console.log(model.y()); // Prints 60.
# model.on(listener)
Listen for changes in configuration. Returns the listener function that can be used to stop listening for changes.
The argument listener is a function of the form listener(configuration), where configuration is the same object returned from model(). This function is invoked after exposed properties are changed.
# model.off(listener)
Stop listening for changes in configuration. The argument listener must be the value returned from on (not the function passed into on).
Serialization
# ReactiveModel.serializeGraph()
Serializes the data flow graph. Returns an object with the following properties.
nodes
An array of objects, each with the following properties.id
The node identifier string.propertyName
The property name. This is the empty string for output nodes of reactive functions with no output property.
links
An array of objects representing edges, each with the following properties.source
The node identifier string of the source node (u).target
The node identifier string of the target node (v).
Example:
var my = ReactiveModel()
("firstName", "Jane")
("lastName", "Smith")
("fullName", function (firstName, lastName){
return firstName + " " + lastName;
}, "firstName, lastName");
var serialized = ReactiveModel.serializeGraph();
The value of serialized
will be:
{
"nodes": [
{ "id": "95", "propertyName": "fullName" },
{ "id": "96", "propertyName": "firstName" },
{ "id": "97", "propertyName": "lastName" }
],
"links": [
{ "source": "96", "target": "95" },
{ "source": "97", "target": "95" }
]
}
See also:
- ReactiveFunction.serializeGraph()
- graph.serialize()
- graph-diagrams
Related Work
- ZJONSSON/clues A very similar library based on Promises.
- AngularJS Dependency Injection Inspired the API for reactive functions.
- AngularJS $digest() Inspired the "digest" term.
- AMD Also inspired the API for reactive functions.
- Notes on Graph Algorithms Used in Optimizing Compilers Algorithms for flow graph analysis.
- d3-scale Inspired documentation style.
- mobx Very similar effort.
- RxJS and Bacon Full blown FRP packages.