Skip to content
/ frb Public

Functional Reactive Bindings (frb): A CommonJS package that includes functional and generic building blocks to help incrementally ensure consistent state.

License

Notifications You must be signed in to change notification settings

montagejs/frb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FRB Logo

Functional Reactive Bindings

npm version

Build Status

In their simplest form, bindings provide the illusion that two objects have the same property. Changing the property on one object causes the same change in the other. This is useful for coordinating state between views and models, among other entangled objects. For example, if you enter text into a text field, the same text might be added to the corresponding database record.

bind(object, "a.b", {"<->": "c.d"});

Functional Reactive Bindings go farther. They can gracefully bind long property paths and the contents of collections. They can also incrementally update the results of chains of queries including maps, flattened arrays, sums, and averages. They can also add and remove elements from sets based on the changes to a flag. FRB makes it easy to incrementally ensure consistent state.

bind(company, "payroll", {"<-": "departments.map{employees.sum{salary}}.sum()"});
bind(document, "body.classList.has('dark')", {"<-": "darkMode", source: viewModel});

FRB is built from a combination of powerful functional and generic building blocks, making it reliable, easy to extend, and easy to maintain.

Getting Started

frb is a CommonJS package, with JavaScript modules suitable for use with Node.js on the server side or Mr on the client side.

❯ npm install frb

Tutorial

In this example, we bind model.content to document.body.innerHTML.

var bind = require("frb/bind");
var model = {content: "Hello, World!"};
var cancelBinding = bind(document, "body.innerHTML", {
    "<-": "content",
    "source": model
});

When a source property is bound to a target property, the target gets reassigned to the source any time the source changes.

model.content = "Farewell.";
expect(document.body.innerHTML).toBe("Farewell.");

Bindings can be recursively detached from the objects they observe with the returned cancel function.

cancelBinding();
model.content = "Hello again!"; // doesn't take
expect(document.body.innerHTML).toBe("Farewell.");

Two-way Bindings

Bindings can go one way or in both directions. Declare one-way bindings with the <- property, and two-way bindings with the <-> property.

In this example, the "foo" and "bar" properties of an object will be inexorably intertwined.

var object = {};
var cancel = bind(object, "foo", {"<->": "bar"});

// <-
object.bar = 10;
expect(object.foo).toBe(10);

// ->
object.foo = 20;
expect(object.bar).toBe(20);

Right-to-left

Note that even with a two-way binding, the right-to-left binding precedes the left-to-right. In this example, "foo" and "bar" are bound together, but both have initial values.

var object = {foo: 10, bar: 20};
var cancel = bind(object, "foo", {"<->": "bar"});
expect(object.foo).toBe(20);
expect(object.bar).toBe(20);

The right-to-left assignment of bar to foo happens first, so the initial value of foo gets lost.

Properties

Bindings can follow deeply nested chains, on both the left and the right side.

In this example, we have two object graphs, foo, and bar, with the same structure and initial values. This binds bar.a.b to foo.a.b and also the other way around.

var foo = {a: {b: 10}};
var bar = {a: {b: 10}};
var cancel = bind(foo, "a.b", {
    "<->": "a.b",
    source: bar
});
// <-
bar.a.b = 20;
expect(foo.a.b).toBe(20);
// ->
foo.a.b = 30;
expect(bar.a.b).toBe(30);

Structure changes

Changes to the structure of either side of the binding are no matter. All of the orphaned event listeners will automatically be canceled, and the binders and observers will reattach to the new object graph.

Continuing from the previous example, we store and replace the a object from one side of the binding. The old b property is now orphaned, and the old b property adopted in its place.

var a = foo.a;
expect(a.b).toBe(30); // from before

foo.a = {}; // orphan a and replace
foo.a.b = 40;
// ->
expect(bar.a.b).toBe(40); // updated

bar.a.b = 50;
// <-
expect(foo.a.b).toBe(50); // new one updated
expect(a.b).toBe(30); // from before it was orphaned

Strings

String concatenation is straightforward.

var object = {name: "world"};
bind(object, "greeting", {"<-": "'hello ' + name + '!'"});
expect(object.greeting).toBe("hello world!");

Sum

Some advanced queries are possible with one-way bindings from collections. FRB updates sums incrementally. When values are added or removed from the array, the sum of only those values is taken and added or removed from the last known sum.

var object = {array: [1, 2, 3]};
bind(object, "sum", {"<-": "array.sum()"});
expect(object.sum).toEqual(6);

Average

The arithmetic mean of a collection can be updated incrementally. Each time the array changes, the added and removed values adjust the last known sum and count of values in the array.

var object = {array: [1, 2, 3]};
bind(object, "average", {"<-": "array.average()"});
expect(object.average).toEqual(2);

Rounding

The round, floor, and ceil methods operate on numbers and return the nearest integer, the nearest integer toward -infinity, and the nearest integer toward infinity respectively.

var object = {number: -0.5};
Bindings.defineBindings(object, {
    "round": {"<-": "number.round()"},
    "floor": {"<-": "number.floor()"},
    "ceil": {"<-": "number.ceil()"}
});
expect(object.round).toBe(0);
expect(object.floor).toBe(-1);
expect(object.ceil).toBe(0);

Last

FRB provides an operator for watching the last value in an Array.

var array = [1, 2, 3];
var object = {array: array, last: null};
Bindings.defineBinding(object, "last", {"<-": "array.last()"});
expect(object.last).toBe(3);

array.push(4);
expect(object.last).toBe(4);

When the dust settles, array.last() is equivalent to array[array.length - 1], but the last observer guarantees that it will not jitter between the ultimate value and null or the penultimate value of the collection. With array[array.length], the underlying may not change its content and length atomically.

var changed = jasmine.createSpy();
PropertyChanges.addOwnPropertyChangeListener(object, "last", changed);
array.unshift(0);
array.splice(3, 0, 3.5);
expect(object.last).toBe(4);
expect(changed).not.toHaveBeenCalled();

array.pop();
expect(object.last).toBe(3);

array.clear();
expect(object.last).toBe(null);

Only

FRB provides an only operator, which can either observe or bind the only element of a collection. The only observer watches a collection for when there is only one value in that collection and emits that value.. If there are multiple values, it emits null.

var object = {array: [], only: null};
Bindings.defineBindings(object, {
    only: {"<->": "array.only()"}
});

object.array = [1];
expect(object.only).toBe(1);

object.array.pop();
expect(object.only).toBe(undefined);

object.array = [1, 2, 3];
expect(object.only).toBe(undefined);

The only binder watches a value. When the value is null, it does nothing. Otherwise, it will update the bound collection such that it only contains that value. If the collection was empty, it adds the value. Otherwise, if the collection did not have the value, it replaces the collection's content with the one value. Otherwise, it removes everything but the value it already contains. Regardless of the means, the end result is the same. If the value is non-null, it will be the only value in the collection.

object.only = 2;
expect(object.array.slice()).toEqual([2]);
// Note that slice() is necessary only because the testing scaffold
// does not consider an observable array equivalent to a plain array
// with the same content

object.only = null;
object.array.push(3);
expect(object.array.slice()).toEqual([2, 3]);

One

Like the only operator, there is also a one operator. The one operator will observe one value from a collection, whatever value is easiest to obtain. For an array, it's the first value; for a sorted set, it's whatever value was most recently found or added; for a heap, it's whatever is on top. However, if the collection is null, undefined, or empty, the result is undefined.

var object = {array: [],