Skip to content


Subversion checkout URL

You can clone with
Download ZIP
Functional Reactive Bindings (frb): A CommonJS package that includes functional and generic building blocks to help incrementally ensure consistent state.
Pull request Compare This branch is 84 commits behind montagejs:master.
Fetching latest commit...
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

FRB Logo

Functional Reactive Bindings

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", {"<-": "{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


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.";

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

model.content = "Hello again!"; // doesn't take

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"});

// <- = 10;

// -> = 20;


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"});

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


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;
// ->
foo.a.b = 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


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()"});


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()"});


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()"});


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.splice(3, 0, 3.5);




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];


object.array = [1, 2, 3];

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;
// 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;
expect(object.array.slice()).toEqual([2, 3]);


You can also create mappings from one array to a new array and an expression to evaluate on each value. The mapped array is bound once, and all changes to the source array are incrementally updated in the target array.

var object = {objects: [
    {number: 10},
    {number: 20},
    {number: 30}
bind(object, "numbers", {"<-": "{number}"});
expect(object.numbers).toEqual([10, 20, 30]);
object.objects.push({number: 40});
expect(object.numbers).toEqual([10, 20, 30, 40]);

Any function, like sum or average, can be applied to the result of a mapping. The straight-forward path would be{number}.sum(), but you can use a block with any function as a short hand, objects.sum{number}.


A filter block generates an incrementally updated array filter. The resulting array will contain only those elements from the source array that pass the test deescribed in the block. As values of the source array are added, removed, or changed such that they go from passing to failing or failing to passing, the filtered array gets incrementally updated to include or exclude those values in their proper positions, as if the whole array were regenerated with array.filter by brute force.

var object = {numbers: [1, 2, 3, 4, 5, 6]};
bind(object, "evens", {"<-": "numbers.filter{!(%2)}"});
expect(object.evens).toEqual([2, 4, 6]);
object.numbers.push(7, 8);
expect(object.evens).toEqual([4, 6, 8]);

Some and Every

A some block incrementally tracks whether some of the values in a collection meet a criterion.

var object = Bindings.defineBindings({
    options: [
        {checked: true},
        {checked: false},
        {checked: false}
}, {
    anyChecked: {
        "<-": "options.some{checked}"

An every block incrementally tracks whether all of the values in a collection meet a criterion.

var object = Bindings.defineBindings({
    options: [
        {checked: true},
        {checked: false},
        {checked: false}
}, {
    allChecked: {
        "<-": "options.every{checked}"

You can use a two-way binding on some and every blocks.

var object = Bindings.defineBindings({
    options: [
        {checked: true},
        {checked: false},
        {checked: false}
}, {
    allChecked: {
        "<->": "options.every{checked}"
    noneChecked: {
        "<->": "!options.some{checked}"

object.noneChecked = true;
expect(object.options.every(function (option) {
    return !option.checked

object.allChecked = true;

The caveat of an equals binding applies. If the condition for every element of the collection is set to true, the condition will be bound incrementally to true on each element. When the condition is set to false, the binding will simply be canceled.

object.allChecked = false;
expect(object.options.every(function (option) {
    return option.checked; // still checked


A sorted block generates an incrementally updated sorted array. The resulting array will contain all of the values from the source except in sorted order.

var object = {numbers: [5, 2, 7, 3, 8, 1, 6, 4]};
bind(object, "sorted", {"<-": "numbers.sorted{}"});
expect(object.sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);

The block may specify a property or expression by which to compare values.

var object = {arrays: [[1, 2, 3], [1, 2], [], [1, 2, 3, 4], [1]]};
bind(object, "sorted", {"<-": "arrays.sorted{-length}"});
expect( (array) {
    return array.slice(); // to clone
    [1, 2, 3, 4],
    [1, 2, 3],
    [1, 2],

The sorted binding responds to changes to the sorted property by removing them at their former place and adding them back at their new position.

object.arrays[0].push(4, 5);
expect( (array) {
    return array.slice(); // to clone
    [1, 2, 3, 4, 5], // new
    [1, 2, 3, 4],
    // old
    [1, 2],

Unique and Sorted

FRB can create a sorted index of unique values using sortedSet blocks.

var object = Bindings.defineBindings({
    folks: [
        {id: 4, name: "Bob"},
        {id: 2, name: "Alice"},
        {id: 3, name: "Bob"},
        {id: 1, name: "Alice"},
        {id: 1, name: "Alice"} // redundant
}, {
    inOrder: {"<-": "folks.sortedSet{id}"},
    byId: {"<-": "{[id, this]}.toMap()"},
    byName: {"<-": "inOrder.toArray().group{name}.toMap()"}



The outcome is a SortedSet data structure, not an Array. The sorted set is useful for fast lookups, inserts, and deletes on sorted, unique data. If you would prefer a sorted array of unique values, you can combine other operators to the same effect.

var object = Bindings.defineBindings({
    folks: [
        {id: 4, name: "Bob"},
        {id: 2, name: "Alice"},
        {id: 3, name: "Bob"},
        {id: 1, name: "Alice"},
        {id: 1, name: "Alice"} // redundant
}, {
    index: {"<-": "{id}.sorted{.0}.map{.1.last()}"}

    {id: 1, name: "Alice"},
    {id: 2, name: "Alice"},
    {id: 3, name: "Bob"},
    {id: 4, name: "Bob"}

Min and Max

A binding can observe the minimum or maximum of a collection. FRB uses a binary heap internally to incrementally track the minimum or maximum value of the collection.

var object = Bindings.defineBindings({}, {
    min: {"<-": "values.min{}"},
    max: {"<-": "values.max{}"}


object.values = [2, 3, 2, 1, 2];

Min and max blocks accept an expression on which to compare values from the collection.

var object = Bindings.defineBindings({}, {
    loser: {"<-": "rounds.min{score}.player"},
    winner: {"<-": "rounds.max{score}.player"}

object.rounds = [
    {score: 0, player: "Luke"},
    {score: 100, player: "Obi Wan"},
    {score: 250, player: "Vader"}

object.rounds[1].score = 300;
expect(object.winner).toEqual("Obi Wan");


FRB can incrementally track equivalence classes within in a collection. The group block accepts an expression that determines the equivalence class for each object in a collection. The result is a nested data structure: an array of [key, class] pairs, where each class is itself an array of all members of the collection that have the corresponding key.

var store = Bindings.defineBindings({}, {
    "clothingByColor": {"<-": "{color}"}
}); = [
    {type: 'shirt', color: 'blue'},
    {type: 'pants', color: 'red'},
    {type: 'blazer', color: 'blue'},
    {type: 'hat', color: 'red'}
    ['blue', [
        {type: 'shirt', color: 'blue'},
        {type: 'blazer', color: 'blue'}
    ['red', [
        {type: 'pants', color: 'red'},
        {type: 'hat', color: 'red'}

Tracking the positions of every key and every value in its equivalence class can be expensive. Internally, group blocks are implemented with a groupMap block followed by an entries() observer. The groupMap produces a Map data structure and does not waste any time, but does not produce range change events. The entries() observer projects the map of classes into the nested array data structure.

You can use the groupMap block directly.

Bindings.cancelBinding(store, "clothingByColor");
Bindings.defineBindings(store, {
    "clothingByColor": {"<-": "clothing.groupMap{color}"}
var blueClothes = store.clothingByColor.get('blue');
    {type: 'shirt', color: 'blue'},
    {type: 'blazer', color: 'blue'}
]);{type: 'gloves', color: 'blue'});
    {type: 'shirt', color: 'blue'},
    {type: 'blazer', color: 'blue'},
    {type: 'gloves', color: 'blue'}

The group and groupMap blocks both respect the type of the source collection. If instead of an array you were to use a SortedSet, the equivalence classes would each be sorted sets. This is useful because replacing values in a sorted set can be performed with much less waste than with a large array.


Suppose that your source is a large data store, like a SortedSet from the Collections package. You might need to view a sliding window from that collection as an array. The view binding reacts to changes to the collection and the position and length of the window.

var SortedSet = require("collections/sorted-set");
var controller = {
    index: SortedSet([1, 2, 3, 4, 5, 6, 7, 8]),
    start: 2,
    length: 4
var cancel = bind(controller, "view", {
    "<-": "index.view(start, length)"

expect(controller.view).toEqual([3, 4, 5, 6]);

// change the window length
controller.length = 3;
expect(controller.view).toEqual([3, 4, 5]);

// change the window position
controller.start = 5;
expect(controller.view).toEqual([6, 7, 8]);

// add content behind the window
expect(controller.view).toEqual([5, 6, 7]);


An enumeration observer produces [index, value] pairs. You can bind to the index or the value in subsequent stages. The prefix dot distinguishes the zeroeth property from the literal zero.

var object = {letters: ['a', 'b', 'c', 'd']};
bind(object, "lettersAtEvenIndexes", {
    "<-": "letters.enumerate().filter{!(.0 % 2)}.map{.1}"
expect(object.lettersAtEvenIndexes).toEqual(['a', 'c']);
expect(object.lettersAtEvenIndexes).toEqual(['b', 'd']);


A range observes a given length and produces and incrementally updates an array of consecutive integers starting with zero with that given length.

var object = Bindings.defineBinding({}, "stack", {
    "<-": "&range(length)"

object.length = 3;
expect(object.stack).toEqual([0, 1, 2]);

object.length = 1;


You can flatten nested arrays. In this example, we have an array of arrays and bind it to a flat array.

var arrays = [[1, 2, 3], [4, 5, 6]];
var object = {};
bind(object, "flat", {
    "<-": "flatten()",
    source: arrays
expect(object.flat).toEqual([1, 2, 3, 4, 5, 6]);

Note that changes to the inner and outer arrays are both projected into the flattened array.

arrays.push([7, 8, 9]);
expect(object.flat).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);

Also, as with all other bindings that produce arrays, the flattened array is never replaced, just incrementally updated.

var flat = object.flat;
arrays.splice(0, arrays.length);
expect(object.flat).toBe(flat); // === same object


You can observe the concatenation of collection of dynamic arrays.

var object = Bindings.defineBinding({
    head: 10,
    tail: [20, 30]
}, "flat", {
    "<-": "[head].concat(tail)"
expect(object.flat).toEqual([10, 20, 30]);

The underlying mechanism is equivalent to [[head], tail].flatten().


You can bind the reversal of an array.

var object = {forward: [1, 2, 3]};
bind(object, "backward", {
    "<->": "forward.reversed()"
expect(object.backward.slice()).toEqual([3, 2, 1]);
expect(object.forward.slice()).toEqual([1, 2, 3, 4]);
expect(object.backward.slice()).toEqual([4, 3, 2, 1]);

Note that you can do two-way bindings, <-> with reversed arrays. Changes to either side are updated to the opposite side.

expect(object.backward.slice()).toEqual([4, 3, 2]);
expect(object.forward.slice()).toEqual([2, 3, 4]);


You can bind a property to always reflect whether a collection contains a particular value.

var object = {
    haystack: [1, 2, 3],
    needle: 3
bind(object, "hasNeedle", {"<-": "haystack.has(needle)"});
object.haystack.pop(); // 3 comes off

The binding also reacts to changes to the value you seek.

// Continued from above...
object.needle = 2;

has bindings are not incremental, but with the right data-structure, updates are cheap. The Collections package contains Lists, Sets, and OrderedSets that all can send content change notifications and thus can be bound.

// Continued from above...
var Set = require("collections/set");
object.haystack = new Set([1, 2, 3]);

has bindings can also be left-to-right and bi-directional.

bind(object, "hasNeedle", {"<->": "haystack.has(needle)"});
object.hasNeedle = false;

The collection on the left-hand-side must implement has or contains, add, and delete or remove. FRB shims Array to have has, add, and delete, just like all the collections in Collections. It happens that the classList properties of DOM elements, when they are supported, implement add, remove, and contains.

var model = {darkMode: false};
bind(document.body, "classList.has('dark')", {
    "<-": "darkMode",
    source: model

The DOM classList does not however implement addContentChangeListener or removeContentChangeListener, so it cannot be used on the right-hand-side of a binding, and such bindings cannot be bidirectional. With some DOM Mutation Observers, you might be able to help FRB overcome this limitation in the future.


A binding can observe changes in key-to-value mappings in arrays and map Collections.

var object = {
    array: [1, 2, 3],
    second: null
var cancel = bind(object, "second", {
    "<->": "array.get(1)"
expect(object.array.slice()).toEqual([1, 2, 3]);

expect(object.array.slice()).toEqual([2, 3]);

object.second = 4;
expect(object.array.slice()).toEqual([2, 4]);

expect(object.second).toBe(4); // still

The source collection can be a Map, Dict, MultiMap, SortedMap, SortedArrayMap, or anything that implements get and addMapChangeListener as specified in Collections. The key can also be a variable.

var Map = require("collections/map");
var a = {id: 0}, b = {id: 1};
var object = {
    source: Map([[a, 10], [b, 20]]),
    key: null,
    selected: null

var cancel = bind(object, "selected", {
    "<-": "source.get(key)"

object.key = a;

object.key = b;

object.source.set(b, 30);

var SortedMap = require("collections/sorted-map");
object.source = SortedMap();

object.source.set(b, 40);

object.key = a; // no effect

You can also bind the entire content of a map-like collection to the content of another. Bear in mind that the content of the source replaces the content of the target initially.

var Map = require("collections/map");
var object = {
    a: Map({a: 10}),
    b: Map()
var cancel = bind(object, "a.mapContent()", {"<->": "b.mapContent()"});

object.a.set('a', 10);
expect(object.a.toObject()).toEqual({a: 10});
expect(object.b.toObject()).toEqual({a: 10});

object.b.set('b', 20);
expect(object.a.toObject()).toEqual({a: 10, b: 20});
expect(object.b.toObject()).toEqual({a: 10, b: 20});

In this case, the source of the binding is a different object than the target, so the binding descriptor specifies the alternate source.

Keys, Values, Entries

If the source of a binding is a map, FRB can also translate changes to the map into changes on an array. The keys, values, and entries observers produce incrementally updated projections of the key-value-mappings onto an array.

var Map = require("collections/map");
var object = Bindings.defineBindings({}, {
    keys: {"<-": "map.keys()"},
    values: {"<-": "map.values()"},
    entries: {"<-": "map.entries()"}
}); = Map({a: 10, b: 20, c: 30});
expect(object.keys).toEqual(['a', 'b', 'c']);
expect(object.values).toEqual([10, 20, 30]);
expect(object.entries).toEqual([['a', 10], ['b', 20], ['c', 30]]);'d', 40);'a');
expect(object.keys).toEqual(['b', 'c', 'd']);
expect(object.values).toEqual([20, 30, 40]);
expect(object.entries).toEqual([['b', 20], ['c', 30], ['d', 40]]);

Coerce to Map

Records (Objects with a fixed shape), arrays of entries, and Maps themselves can be coerced to an incrementally updated Map with the toMap operator.

var object = Bindings.defineBindings({}, {
    map: {"<-": "entries.toMap()"}

// map property will persist across changes to entries
var map =;

object.entries = {a: 10};

The toMap observer maintains the insertion order of the keys.

// Continued...
object.entries = [['b', 20], ['c', 30]];
expect(map.keys()).toEqual(['b', 'c']);

expect(map.keys()).toEqual(['c', 'b']);

If the entries do not have unique keys, the last entry wins. This is managed internally by observing,{.0}.map{.1.last()}.

// Continued...
object.entries = [['a', 10], ['a', 20]];

toMap binds the content of the output map to the content of the input map and will clear and repopulate the output map if the input map is replaced.

// Continued...
object.entries = new Map({a: 10});


You can bind to whether expressions are equal.

var fruit = {apples: 1, oranges: 2};
bind(fruit, "equal", {"<-": "apples == oranges"});
expect(fruit.equal).toBe(false); = 1;

Equality can be bound both directions. In this example, we do a two-way binding between whether a radio button is checked and a corresponding value in our model.

var component = {
    orangeElement: {checked: false},
    appleElement: {checked: true}
Bindings.defineBindings(component, {
    "orangeElement.checked": {"<->": "fruit == 'orange'"},
    "appleElement.checked": {"<->": "fruit == 'apple'"},

component.orangeElement.checked = true;

component.appleElement.checked = true;

Because equality and assignment are interchanged in this language, you can use either = or ==.

FRB also supports a comparison operator, <=>, which uses to determines how two operands should be sorted in relation to each other.

Array and Map Content

In JavaScript, arrays behave both like objects (in the sense that every index is a property, but also like a map collection of index-to-value pairs. The Collections package goes so far as to patch up the Array prototype so arrays can masquerade as maps, with the caveat that delete(value) behaves like a Set instead of a Map.

This duplicity is reflected in FRB. You can access the values in an array using the object property notation or the mapped key notation.

var object = {
    array: [1, 2, 3]
Bindings.defineBindings(object, {
    first: {"<-": "array.0"},
    second: {"<-": "array.get(1)"}

To distinguish a numeric property of the source from a number literal, use a dot. To distingish a mapped index from an array literal, use an empty expression.

var array = [1, 2, 3];
var object = {};
Bindings.defineBindings(object, {
    first: {
        "<-": ".0",
        source: array
    second: {
        "<-": "get(1)",
        source: array

Unlike property notation, map notation can observe a variable index.

var object = {
    array: [1, 2, 3],
    index: 0
Bindings.defineBinding(object, "last", {
    "<-": "array.get(array.length - 1)"


You can also bind all of the content of an array by range or by mapping. The notation for binding ranged content is rangeContent(). Every change to an Array or SortedSet dispatches range changes and any collection that implements splice and swap can be a target for such changes.

var SortedSet = require("collections/sorted-set");
var object = {
    set: SortedSet(),
    array: []
Bindings.defineBindings(object, {
    "array.rangeContent()": {"<-": "set"}
object.set.addEach([5, 2, 6, 1, 4, 3]);
expect(object.array).toEqual([1, 2, 3, 4, 5, 6]);

The notation for binding the content of any mapping collection using map changes is mapContent(). On the target of a binding, this will note when values are added or removed on each key of the source collection and apply the same change to the target. The target and source can be arrays or map collections.

var Map = require("collections/map");
var object = {
    map: Map(),
    array: []
Bindings.defineBinding(object, "map.mapContent()", {
    "<-": "array"
object.array.push(1, 2, 3);
    0: 1,
    1: 2,
    2: 3


A note about the source value: an empty path implies the source value. Using empty paths and empty expressions is useful in some situations.

If a value is ommitted on either side of an operator, it implies the source value. The expression sorted{} indicates a sorted array, where each value is sorted by its own numeric value. The expression filter{!!} would filter falsy values. The operand is implied. Similarly, filter{!(%2)} produces only even values.

This is why you can use .0 to get the zeroth property of an array, to distingiush the form from 0 which would be a numeric literal, and why you can use ()[0] to map the zeroeth key of a map or array, to distinguish the form from [0] which would be an array literal.

With Context Value

Expressions can be evaluated in the context of another value using a variant of property notation. A parenthesized expression can follow a path.

var object = {
    context: {a: 10, b: 20}
Bindings.defineBinding(object, "sum", {
    "<-": "context.(a + b)"

Bindings.cancelBinding(object, "sum");
object.context.a = 20;
expect(object.sum).toBe(30); // unchanged

To observe a constructed array or object literal, the expression does not need parentheses.

var object = {
    context: {a: 10, b: 20}
Bindings.defineBindings(object, {
    "duple": {"<-": "context.[a, b]"},
    "pair": {"<-": "context.{key: a, value: b}"}
expect(object.duple).toEqual([10, 20]);
expect(object.pair).toEqual({key: 10, value: 20});



FRB can also recognize many operators. These are in order of precedence unary - negation, + numeric coercion, and ! logical negation and then binary ** power, // root, %% logarithm, *, /, % modulo, %% remainder, +, -, <, >, <=, >=, = or ==, !=, && and ||.

var object = {height: 10};
bind(object, "heightPx", {"<-": "height + 'px'"});

The unary + operator coerces a value to a number. It is handy for binding a string to a number.

var object = {
    number: null,
    string: null,
Bindings.defineBinding(object, "+number", {
    "<-": "string"
object.string = '10';


FRB supports some common functions. startsWith, endsWith, and contains all operate on strings. join concatenates an array of strings with a given delimiter (or empty string). split breaks a string between every delimiter (or just between every character). join and split are algebraic and can be bound as well as observed.


FRB supports the ternary conditional operator, if ? then : else.

var object = Bindings.defineBindings({
    condition: null,
    consequent: 10,
    alternate: 20
}, {
    choice: {"<->": "condition ? consequent : alternate"}

expect(object.choice).toBe(undefined); // no choice made

object.condition = true;

object.condition = false;

The ternary operator can bind in both directions.

object.choice = 30;

object.condition = true;
object.choice = 40;


FRB can automatically invert algebraic operators as long as they operate strictly on the left-most expressions on both the source and target are bindable properties.

In this example, the primary binding is notToBe <- !toBe, and the inverse binding is automatically computed toBe <- !notToBe.

var caesar = {toBe: false};
bind(caesar, "notToBe", {"<->": "!toBe"});

caesar.notToBe = false;

FRB does algebra by rotating the expressions on one side of a binding to the other until only one independent property remains (the left most expression) on the target side of the equation.

convert: y <- !x
revert: x <- !y
convert: y <- x + a
revert: x <- y - a

The left-most independent variable on the right hand side becomes the dependent variable on the inverted binding. At present, this only works for numbers and when the left-most expression is a bindable property because it cannot assign a new value to the literal 10. For example, FRB cannot yet implicitly revert y <-> 10 + x.


You may have noticed literals in the previous examples. String literals take the form of any characters between single quotes. Any character can be escaped with a back slash.

var object = {};
bind(object, "greeting", {"<-": "'Hello, World!'"});
expect(object.greeting).toBe("Hello, World!");

Number literals are digits with an optional mantissa.

bind(object, 'four', {"<-": "2 + 2"});


Bindings can produce fixed-length arrays. These are most useful in conjunction with mappings. Tuples are comma-delimited and parantheses-enclosed.

var object = {array: [[1, 2, 3], [4, 5]]};
bind(object, "summary", {"<-": "{[length, sum()]}"});
    [3, 6],
    [2, 9]


Bindings can also produce fixed-shape objects. The notation is comma-delimited, colon-separated entries, enclosed by curly-braces.

var object = {array: [[1, 2, 3], [4, 5]]};
bind(object, "summary", {
    "<-": "{{length: length, sum: sum()}}"
    {length: 3, sum: 6},
    {length: 2, sum: 9}

The left hand side of an entry in a record is any combination of letters or numbers. The right side is any expression.


Bindings can also involve parameters. The source of parameters is by default the same as the source. The source, in turn, defaults to the same as the target object. It can be specified on the binding descriptor. Parameters are declared by any expression following a dollar sign.

var object = {a: 10, b: 20, c: 30};
bind(object, "foo", {
    "<-": "[$a, $b, $c]"},
    parameters: object

Bindings also react to changes to the parameters.

object.a = 0;
object.b = 1;
object.c = 2;
expect([0, 1, 2]);

The degenerate case of the property language is an empty string. This is a valid property path that observes the value itself. So, as an emergent pattern, a $ expression by itself corresponds to the whole parameters object.

var object = {};
bind(object, "ten", {"<-": "$", parameters: 10});

Elements and Components

FRB provides a # notation for reaching into the DOM for an element. This is handy for binding views and models on a controller object.

The defineBindings method accepts an optional final argument, parameters, which is shared by all bindings (unless shadowed by a more specific parameters object on an individual descriptor).

The parameters can include a document. The document may be any object that implements getElementById.

Additionally, the frb/dom is an experiment that monkey-patches the DOM to make some properties of DOM elements observable, like the value or checked attribute of an input or textarea element.

var Bindings = require("frb");

var controller = Bindings.defineBindings({}, {

    "fahrenheit": {"<->": "celsius * 1.8 + 32"},
    "celsius": {"<->": "kelvin - 272.15"},

    "#fahrenheit.value": {"<->": "+fahrenheit"},
    "#celsius.value": {"<->": "+celsius"},
    "#kelvin.value": {"<->": "+kelvin"}

}, {
    document: document

controller.celsius = 0;

One caveat of this approach is that it can cause a lot of DOM repaint and reflow events. The Montage framework uses a synchronized draw cycle and a component object model to minimize the cost of computing CSS properties on the DOM and performing repaints and reflows, deferring such operations to individual animation frames.

For a future release of Montage, FRB provides an alternate notation for reaching into the component object model, using its deserializer. The @ prefix refers to another component by its label. Instead of providing a document, Montage provides a serialization, which in turn implements getObjectForLabel.

var Bindings = require("frb");

var controller = Bindings.defineBindings({}, {

    "fahrenheit": {"<->": "celsius * 1.8 + 32"},
    "celsius": {"<->": "kelvin - 272.15"},

    "@fahrenheit.value": {"<->": "+fahrenheit"},
    "@celsius.value": {"<->": "+celsius"},
    "@kelvin.value": {"<->": "+kelvin"}

}, {
    serializer: serializer

controller.celsius = 0;


FRB’s bindings use observers and binders internally. You can create an observer from a property path with the observe function exported by the frb/observe module.

var results = [];
var object = {foo: {bar: 10}};
var cancel = observe(object, "", function (value) {
}); = 10;
expect(results).toEqual([10]); = 20;
expect(results).toEqual([10, 20]);

For more complex cases, you can specify a descriptor instead of the callback. For example, to observe a property’s value before it changes, you can use the beforeChange flag.

var results = [];
var object = {foo: {bar: 10}};
var cancel = observe(object, "", {
    change: function (value) {
    beforeChange: true

expect(results).toEqual([10]); = 20;
expect(results).toEqual([10, 10]); = 30;
expect(results).toEqual([10, 10, 20]);

If the product of an observer is an array, that array is always updated incrementally. It will only get emitted once. If you want it to get emitted every time its content changes, you can use the contentChange flag.

var lastResult;
var array = [[1, 2, 3], [4, 5, 6]];
observe(array, "map{sum()}", {
    change: function (sums) {
        lastResult = sums.slice();
        // 1. [6, 15]
        // 2. [6, 15, 0]
        // 3. [10, 15, 0]
    contentChange: true

expect(lastResult).toEqual([6, 15]);

expect(lastResult).toEqual([6, 15, 0]);

expect(lastResult).toEqual([10, 15, 0]);

Nested Observers

To get the same effect as the previous example, you would have to nest your own content change observer.

var i = 0;
var array = [[1, 2, 3], [4, 5, 6]];
var cancel = observe(array, "map{sum()}", function (array) {
    function contentChange() {
        if (i === 0) {
            expect(array.slice()).toEqual([6, 15]);
        } else if (i === 1) {
            expect(array.slice()).toEqual([6, 15, 0]);
        } else if (i === 2) {
            expect(array.slice()).toEqual([10, 15, 0]);
    return function cancelContentChange() {

This illustrates one crucial aspect of the architecture. Observers return cancelation functions. You can also return a cancelation function inside a callback observer. That canceler will get called each time a new value is observed, or when the parent observer is canceled. This makes it possible to nest observers.

var object = {foo: {bar: 10}};
var cancel = observe(object, "foo", function (foo) {
    return observe(foo, "bar", function (bar) {


FRB provides utilities for declaraing and managing multiple bindings on objects. The frb (frb/bindings) module exports this interface.

var Bindings = require("frb");

The Bindings module provides defineBindings and cancelBindings, defineBinding and cancelBinding, as well as binding inspector methods getBindings and getBinding. All of these take a target object as the first argument.

The Bindings.defineBinding(target, descriptors) method returns the target object for convenience.

var target = Bindings.defineBindings({}, {
    "fahrenheit": {"<->": "celsius * 1.8 + 32"},
    "celsius": {"<->": "kelvin - 272.15"}
target.celsius = 0;

Bindings.getBindings in that case would return an object with fahrenheit and celsius keys. The values would be identical to the given binding descriptor objects, like {"<->": "kelvin - 272.15"}, but it also gets annotated with a cancel function and the default values for any ommitted properties like source (same as target), parameters (same as source), and others.

Bindings.cancelBindings cancels all bindings attached to an object and removes them from the bindings descriptors object.


Binding Descriptors

Binding descriptors describe the source of a binding and additional parameters. Bindings.defineBindings can set up bindings (<- or <->), computed (compute) properties, and falls back to defining ES5 properties with permissive defaults (enumerable, writable, and configurable all on by default).

If a descriptor has a <- or <->, it is a binding descriptor. FRB creates a binding, adds the canceler to the descriptor, and adds the descriptor to an internal table that tracks all of the bindings defined on that object.

var object = Bindings.defineBindings({
    darkMode: false,
    document: document
}, {
    "document.body.classList.has('dark')": {
        "<-": "darkMode"

You can get all the binding descriptors with Bindings.getBindings, or a single binding descriptor with Bindings.getBinding. Bindings.cancel cancels all the bindings to an object and Bindings.cancelBinding will cancel just one.

// Continued from above...
var bindings = Bindings.getBindings(object);
var descriptor = Bindings.getBinding(object, "document.body.classList.has('dark')");
Bindings.cancelBinding(object, "document.body.classList.has('dark')");


A binding descriptor can have a convert function, a revert function, or alternately a converter object. Converters are useful for transformations that cannot be expressed in the property language, or are not reversible in the property language.

In this example, a and b are synchronized such that a is always half of b, regardless of which property gets updated.

var object = Bindings.defineBindings({
    a: 10
}, {
    b: {
        "<->": "a",
        convert: function (a) {
            return a * 2;
        revert: function (b) {
            return b / 2;

object.b = 10;

Converter objects are useful for reusable or modular converter types and converters that track additional state.

function Multiplier(factor) {
    this.factor = factor;
Multiplier.prototype.convert = function (value) {
    return value * this.factor;
Multiplier.prototype.revert = function (value) {
    return value / this.factor;

var doubler = new Multiplier(2);

var object = Bindings.defineBindings({
    a: 10
}, {
    b: {
        "<->": "a",
        converter: doubler

object.b = 10;

Reusable converters have an implied direction, from some source type to a particular target type. Sometimes the types on your binding are the other way around. For that case, you can use the converter as a reverter. This merely swaps the convert and revert methods.

var uriConverter = {
    convert: encodeURI,
    revert: decodeURI
var model = Bindings.defineBindings({}, {
    "title": {
        "<->": "location",
        reverter: uriConverter

model.title = "Hello, World!";

model.location = "Hello,%20Dave.";
expect(model.title).toEqual("Hello, Dave.");

Computed Properties

A computed property is one that gets updated with a function call when one of its arguments changes. Like a converter, it is useful in cases where a transformation or computation cannot be expressed in the property language, but can additionally accept multiple arguments as input. A computed property can be used as the source for another binding.

In this example, we create an object as the root of multiple bindings. The object synchronizes the properties of a "form" object with the window’s search string, effectively navigating to a new page whenever the "q" or "charset" values of the form change.

    window: window,
    form: {
        q: "",
        charset: "utf-8"
}, {
    queryString: {
        args: ["form.q", "form.charset"],
        compute: function (q, charset) {
            return "?" + QS.stringify({
                q: q,
                charset: charset
    "": {
        "<-": "queryString"

Debugging with Traces

A binding can be configured to log when it changes and why. The trace property on a descriptor instructs the binder to log changes to the console.

    a: 10
}, {
    b: {
        "<-": "a + 1",


Functional Reactive Bindings is an implementation of synchronous, incremental object-property and collection-content bindings for JavaScript. It was ripped from the heart of the Montage web application framework and beaten into this new, slightly magical form. It must prove itself worthy before it can return.

  • functional: The implementation uses functional building blocks to compose observers and binders.
  • generic: The implementation uses generic methods on collections, like addContentChangeListener, so any object can implement the same interface and be used in a binding.
  • reactive: The values of properties and contents of collections react to changes in the objects and collections on which they depend.
  • synchronous: All bindings are made consistent in the statement that causes the change. The alternative is asynchronous, where changes are queued up and consistency is restored in a later event.
  • incremental: If you update an array, it produces a content change which contains the values you added, removed, and the location of the change. Most bindings can be updated using only these values. For example, a sum is updated by decreasing by the sum of the values removed, and increasing by the sum of the values added. FRB can incrementally update map, reversed, flatten, sum, and average observers. It can also incrementally update has bindings.
  • unwrapped: Rather than wrap objects and arrays with observable containers, FRB modifies existing arrays and objects to make them dispatch property and content changes. For objects, this involves installing getters and setters using the ES5 Object.defineProperty method. For arrays, this involves replacing all of the mutation methods, like push and pop, with variants that dispatch change notifications. The methods are either replaced by swapping the __proto__ or adding the methods to the instance with Object.defineProperties. These techniques should [work]Define Property starting in Internet Explorer 9, Firefox 4, Safari 5, Chrome 7, and Opera 12.


  • Collections provides property, mapped content, and ranged content change events for objects, arrays, and other collections. For objects, this adds a property descriptor to the observed object. For arrays, this either swaps the prototype or mixes methods into the array so that all methods dispatch change events.
    Caveats: you have to use a set method on Arrays to dispatch property and content change events. Does not work in older Internet Explorers since they support neither prototype assignment or ES5 property setters.
  • observer functions for watching an entire object graph for incremental changes, and gracefully rearranging and canceling those observers as the graph changes. Observers can be constructed directly or with a very small query language that compiles to a tree of functions so no parsing occurs while the graph is being watched.
  • one- and two-way bindings using binder and obserer functions to incrementally update objects.
  • declarative interface for creating an object graph with bindings, properties, and computed properties with dependencies.


The highest level interface for FRB resembles the ES5 Object constructor and can be used to declare objects and define and cancel bindings on them with extended property descriptors.

var Bindings = require("frb");

// create an object
var object = Bindings.defineBindings({
    foo: 0,
    graph: [
        {numbers: [1,2,3]},
        {numbers: [4,5,6]}
}, {
    bar: {"<->": "foo", enumerable: false},
    numbers: {"<-": "{numbers}.flatten()"},
    sum: {"<-": "numbers.sum()"},
    reversed: {"<-": "numbers.reversed()"}

expect(; = 10;
expect(; = 20;

// note that the identity of the bound numbers array never
// changes, because all of the changes to that array are
// incrementally updated
var numbers = object.numbers;

// first computation

// adds an element to graph,
// which pushes [7, 8, 9] to "{numbers}",
// which splices [7, 8, 9] to the end of
//  "{numbers}.flatten()",
// which increments "sum()" by [7, 8, 9].sum()
object.graph.push({numbers: [7, 8, 9]});

// splices [1] to the beginning of [1, 2, 3],
// which splices [1] to the beginning of "...flatten()"
// which increments "sum()" by [1].sum()

// cancels the entire observer hierarchy, then attaches
//  listeners to the new one.  updates the sum.
object.graph = [{numbers: [1,2,3]}];

expect(object.reversed).toEqual([3, 2, 1]);

expect(object.numbers).toBe(numbers) // still the same object

Bindings.cancelBindings(object); // cancels all bindings on this object and
// their transitive observers and event listeners as deep as
// they go
  • Bindings.defineBindings(object, name, descriptor)
  • Bindings.defineBinding(object, name, descriptor)
  • Bindings.getBindings(object)
  • Bindings.getBinding(object, name)
  • Bindings.cancelBindings(object)
  • Bindings.cancelBinding(object, name)

A binding descriptor contains:

  • target: the
  • targetPath: the target
  • targetSyntax: the syntax tree for the target path
  • source: the source object, which defaults to target
  • sourcePath: the source path, from either <- or <->
  • sourceSyntax: the syntax tree for the source path
  • twoWay: whether the binding goes in both directions, if <-> was the source path.
  • parameters: the parameters, which default to source.
  • convert: a function that converts the source value to the target value, useful for coercing strings to dates, for example.
  • revert: a function that converts the target value to the source value, useful for two-way bindings.
  • converter: an object with convert and optionally also a revert method. The implementation binds these methods to their converter and stores them in covert and revert.
  • serializable: a note from the Montage Deserializer, to the Montage Serializer, indicating that the binding came from a serialization, and to a serialization it must return.
  • cancel: a function to cancel the binding


The bind module provides direct access to the bind function.

var bind = require("frb/bind");

var source = [{numbers: [1,2,3]}, {numbers: [4,5,6]}];
var target = {};
var cancel = bind(target, "summary", {
    "<-": "map{[numbers.sum(), numbers.average()]}",
    source: source

    [6, 2],
    [15, 5]


bind is built on top of parse, compileBinder, and compileObserver.


The compute module provides direct access to the compute function, used by Bindings to make computed properties.

var compute = require("frb/compute");

var source = {operands: [10, 20]};
var target = {};
var cancel = compute(target, "sum", {
    source: source,
    args: ["operands.0", "operands.1"],
    compute: function (a, b) {
        return a + b;


// change one operand
source.operands.set(1, 30); // needed to dispatch change notification


The observe modules provides direct access to the observe function. observe is built on top of parse and compileObserver. compileObserver creates a tree of observers using the methods in the observers module.

var observe = require("frb/observe");

var source = [1, 2, 3];
var sum;
var cancel = observe(source, "sum()", function (newSum) {
    sum = newSum;



source.unshift(0); // no change

source.splice(0, source.length); // would change

observe produces a cancelation hierarchy. Each time a value is removed from an array, the underlying observers are canceled. Each time a property is replaced, the underlying observer is canceled. When new values are added or replaced, the observer produces a new canceler. The cancel function returned by observe commands the entire underlying tree.

Observers also optional accept a descriptor argument in place of a callback.

  • set: the change handler, receives value for most observers, but also key and object for property changes.
  • parameters: the value for $ expressions.
  • beforeChange: instructs an observer to emit the previous value before a change occurs.
  • contentChange: instructs an observer to emit an array every time its content changes. By default, arrays are only emitted once.
var object = {};
var cancel = observe(object, "array", {
    change: function (value) {
        // may return a cancel function for a nested observer
    parameters: {},
    beforeChange: false,
    contentChange: true

object.array = []; // emits []
object.array.push(10); // emits [10]


The compile-evaluator module returns a function that accepts a syntax tree and returns an evaluator function. The evaluator accepts a scope (which may include a value, parent scope, parameters, a document, and components) and returns the corresponding value without all the cost or benefit of setting up incremental observers.

var parse = require("frb/parse");
var compile = require("frb/compile-evaluator");
var Scope = require("frb/scope");

var syntax = parse("a.b");
var evaluate = compile(syntax);
var c = evaluate(new Scope({a: {b: 10}}))

The evaluate module returns a function that accepts a path or syntax tree, a source value, and parameters and returns the corresponding value.

var evaluate = require("frb/evaluate");
var c = evaluate("a.b", {a: {b: 10}})


The stringify module returns a function that accepts a syntax tree and returns the corresponding path in normal form.

var stringify = require("frb/stringify");

var syntax = {type: "and", args: [
    {type: "property", args: [
        {type: "value"},
        {type: "literal", value: "a"}
    {type: "property", args: [
        {type: "value"},
        {type: "literal", value: "b"}

var path = stringify(syntax);
expect(path).toBe("a && b");


  • expression = logical-or-expression
  • conditional-expression = logical-or-expression ( ? expression : expression )?
  • logical-or-expression = logical-and-expression ( || (or) relation expression )?
  • logical-and-expression = relation-expression ( && (and) relation-expression )?
  • relation-expression = arithmetic expression ( relation-operator arithmetic-expression )?
    • relation-operator = == (equals) or < (lt) or <= (le) or > (gt) or >= (ge) or <=> (compare)
  • arithmetic-expression = multiplicative-expresion delimited by arithmetic-operator
    • arithmetic-operator = + (add) or - (sub)
  • multiplicative-expression = exponential-expression delimited by multiplicative-operator
    • multiplicative-operator = * (mul) or / (div) or % (mod) or rem (rem)
  • exponential-expression = unary-expression delimited by exponential-operator
    • exponential-operator = ** (pow) or // (root) or %% (log)
  • unary-expression = unary-operator ? path-expression
    • unary-operator = + (number) or - (neg) or ! (not)
  • path-expression =
    • literal (literal with value) or
    • array-expression (tuple) or
    • object-expression (record) or
    • ( expression ) tail-expression or
    • property-name tail-expression (property) or
    • function-call (piped) tail-expression or
    • block-call tail-expression or
    • # element-id tail-expression (element by id) or
    • @ component-label tail-expression (component by label)
    • & function-call (bare) tail-expression or
  • tail-expression =
    • property-expression or
    • with-expression or
    • variable-property-expression
  • property-expression = . property-name tail-expression (property)
  • with-expression = .
    • ( expression ) tail-expression or
    • array-expression tail-expression or
    • object-expression tail-expression
  • variable-property-expression = [ expression ] tail-expression (property)
  • array-expression = [ ( expression or () (value) ) delimited by , ] (tuple with each expression in args array)
  • object-expression = { (property-name : expression) delimited by , } (record, with each expression as a value in an args object instead of array)
  • property-name = ( non-space-character )+
  • function-call = function-name ( expression delimited by , )
    • function-name = flatten or reversed or enumerate or sum or average or has or view or startsWith or endsWith or contains or join or split or range or keys or values or entries (eponymous syntax node types)
  • block-call = function-name { expression }
    • block-name = map (mapBlock) or filter (filterBlock) or some (someBlock) or every (everyBlock) or sorted (sortedBlock) or sortedSet (sortedSetBlock) or min (minBlock) or max (maxBlock) or group (groupBlock) or groupMap (groupMapBlock) or function-name (map followed by function-call)
  • literal = string-literal or number-literal
    • number-literal = digits ( . digits )? (literal and value is a number)
    • string-literal = ' ( non-quote-character or \ character ) ' (literal and value is a string)


  • terms-of-the-grammar
  • tokens
  • (corresponding syntax node type name)
  • (group)
  • definition =
  • or
  • delimited by
  • optional?
  • any*
  • some+


An expression is observed with a source value and emits a target one or more times. All expressions emit an initial value. Array targets are always updated incrementally. Numbers and boolean are emited anew each time their value changes.

If any operand is null or undefine, a binding will not emit an update. Thus, if a binding’s source becomes invalid, it does not corrupt its target but waits until a valid replacement becomes available.

  • Literals are interpreted as their corresponding value.
  • Value terms provide the source.
  • Parameters terms provide the parameters.
  • In a path-expression, the first term is evaluated with the source value.
  • Each subsequent term of a path expression uses the target of the previous as its source.
  • A property-expression or variable-property-expression observes the key of the source object using Object.addPropertyChangeListener.
  • An element identifier (with the # prefix) uses the document property of the parameters object and emits document.getElementById(id), or dies trying. Changes to the document are not observed.
  • A component label (with the @ prefix) uses the serialization property of parameters object and emits serialization.getObjectForLable(label), or dies trying. Changes to the serialization are not observed. This syntax exists to support Montage serializations.
  • A "map" block observes the source array and emits a target array. The target array is emitted once and all subsequent updates are reflected as content changes that can be independently observed with addContentChangeListener. Each element of the target array corresponds to the observed value of the block expression using the respective element in the source array as the source value.
  • A "map" function call receives a function as its argument rather than a block.
  • A "filter" block observes the source array and emits a target array containing only those values from the source array that actively pass the predicate described in the block expression useing the respective element in the source array as the source value. As with "map", filters update the target array incrementally.
  • A "some" block observes whether any of the values in the source collection meet the given criterion.
  • A "every" block observes whether all of the values in the source collection meet the given criterion.
  • A "sorted" block observes the sorted version of an array, by a property of each value described in the block, or itself if empty. Sorted arrays are incrementally updating as values are added and deleted from the source.
  • A "sortedSet" block observes a collection that emits range change events, by way of a property of each value described in the block, or itself if empty, emitting a SortedSet value exactly once. If the input is or becomes invalid, the sorted set is cleared, not replaced. The sorted set will always contain the last of each group of equivalant values from the input.
  • A "min" block observes the which of the values in a given collection produces the smallest value through the given relation.
  • A "max" block observes the which of the values in a given collection produces the largest value through the given relation.
  • A "group" block observes which values belong to corresponding equivalence classes as determined by the result of a given expression on each value. The observer is responsible for adding and removing classes as they are populated and depopulated. Each class tracks the key (result of the block expression for every member of a class), and an the values of the corresponding class as an array. Values are added to the end of each array as they are discovered.
  • Any function call with a "block" implies calling the function on the result of a "map" block.
  • A "flatten" function call observes a source array and produces a target array. The source array must only contain inner arrays. The target array is emitted once and all subsequent updates can be independently observed with addContentChangeListener. The target array will always contain the concatenation of all of the source arrays. Changes to the inner and outer source arrays are reflected with incremental splices to the target array in their corresponding positions.
  • A "reversed" function call observes the source array and produces a target array that contains the elements of the source array in reverse order. The target is incrementally updated.
  • An "enumerate" expression observes [key, value] pairs from an array. The output array of arrays is incrementally updated with range changes from the source.
  • A "view" function call observes a sliding window from the source, from a start index (first argument) of a certain length (second argument). The source can be any collection that dispatches range changes and the output will be an array of the given length.
  • A "sum" function call observes the numeric sum of the source array. Each alteration to the source array causes a new sum to be emitted, but the sum is computed incrementally by observing the smaller sums of the spliced values, added and removed.
  • An "average" function call observes the average of the input values, much like "sum".
  • A "has" function call observes the source collection for whether it contains an observed value.
  • A "tuple" expression observes a source value and emits a single target array with elements corresponding to the respective expression in the tuple. Each inner expression is evaluated with the same source value as the outer expression.
  • A "startsWith" function call observes whether the left string starts with the right string.
  • An "endsWith" function call observes whether the right string ends with the right string.
  • A "contains" function call observes whether the left string contains the right string.
  • A "join" function observes the left array joined by the right delimiter, or an empty string. This is not an incremental operation.
  • A "split" function observes the left string broken into an array between the right delimiter, or an empty string. This is not an incremental operation.
  • A "range" function call observes an array with the given length containing sequential numbers starting with zero. The output array is updated incrementally and will dispatch one range change each time the size changes by any difference.
  • A "keys" function call observes an incrementally updated array of the keys that a given map contains. The keys are maintained in insertion order.
  • A "values" function call observes an incrementally updated array of the values that a given map contains. The values are maintained in insertion order.
  • An "entries" function call observes an incrementally updated array of [key, value] pairs from a given mapping. The entries are retained in insertion order.

Unary operators:

  • "number" coerces the value to a number.
  • "neg" converts a number to its negative.
  • "not" converts a boolean to its logical opposite.

Binary operators:

  • "add" adds the left to the right
  • "sub" subtracts the right from the left
  • "mul" multiples the left to the right
  • "div" divides the left by the right
  • "mod" produces the left modula the right. This is proper modula, meaning a negative number that does not divide evenly into a positive number will produce the difference between that number and the next evenly divisible number in direction of negative infinity.
  • "rem" produces the remainder of dividing the left by the right. If the left does not divide evenly into the right it will produce the difference between that number and the next evenly divisible number in the direction of zero. That is to say, rem can produce negative numbers.
  • "pow" raises the left to the power of the right.
  • "root" produces the "righth" root of the left.
  • "log" produces the logarithm of the left on the right base.
  • "lt" less than, as determined with, right) < 0.
  • "le" less than or equal, as determined with, right) <= 0.
  • "gt" greater than, as determined with, right) > 0.
  • "ge" greater than or equal, as determined with, right) >= 0.
  • "compare" as determined by, right).
  • "equals" whether the left is equal to the right as determined by Object.equals(left, right).
  • *Note: there is no "not equals" syntax node. The != operator gets converted into a "not" node around an "equals" node.
  • "and" logical union
  • "or" logical intersection

Ternary operator:

  • "if" observes the condition (first argument, expression before the ?). If the expression is true, the result observes the consequent expression (second argument, between the question mark and the colon), and if it is false, the result observes the alternate (the third argument, after the colon). If the condition is null or undefined, the result is null or undefined.

On the left hand side of a binding, the last term has alternate semantics. Binders receive a target as well as a source.

  • A "property" observes an object and a property name from the target, and a value from the source. When any of these change, the binder upates the value for the property name of the object.
  • A "get" observes a collection and a key from the target, and a value from the source. When any of these change, the binder updates the value for the key on the collection using collection.set(key, value). This is suitable for arrays and custom map Collections.
  • A "equals" expression observes a boolean value from the source. If that boolean becomes true, the equality expression is made true by assigning the right expression to the left property of the equality, turning the "equals" into an "assign" conceptually. No action is taken if the boolean becomes false.
  • A "reversed" expression observes an indexed collection and maintains a mirror array of that collection.
  • A "has" function call observes a boolean value from the source, and an collection and a sought value from the target. When the value is true and the value is absent in the collection, the binder uses the add method of the collection (provided by a shim for arrays) to make it true that the collection contains the sought value. When the value is false and the value does appear in the collection one or more times, the binder uses the delete or remove method of the collection to remove all occurrences of the sought value.
  • An "if" binding observes the condition and binds the target either to the consequent or alternate. If the condition is null or undefined, the target is not bound.

If the target expression ends with .*, the content of the target is bound instead of the property. This is useful for binding the content of a non-array collection to the content of another indexed collection. The collection can be any collection that implements the "observable content" interface including dispatchContentChange(plus, minus, index), addContentChangeListener, and removeContentChangeListener.

Language Interface

var parse = require("frb/parse");
var compileObserver = require("frb/compile-observer");
var compileBinder = require("frb/compile-binder");
  • parse(text) returns a syntax tree.
  • compileObserver(syntax) returns an observer function of the form observe(callback, source, parameters) which in turn returns a cancel() function. compileObserver visits the syntax tree and creates functions for each node, using the observers module.
  • compileBinder(syntax) returns a binder function of the form bind(observeValue, source, target, parameters) which in turn returns a cancel() function. compileBinder visits the root node of the syntax tree and delegates to compileObserver for its terms. The root node must be a property at this time, but could conceivably be any function with a clear inverse operation like map and reversed.

Syntax Tree

The syntax tree is JSON serializable and has a "type" property. Nodes have the following types:

  • value corresponds to observing the source value
  • parameters corresponds to observing the parameters object
  • literal has a value property and observes that value
  • element has an id property and observes an element from the parameters.document, by way of getElementById.
  • component has a label property and observes a component from the parameters.serialization, by way of getObjectForLabel. This feature support's Montage’s serialization format.

All other node types have an "args" property that is an array of syntax nodes (or an "args" object for record).

  • property: corresponds to observing a property named by the right argument of the left argument.
  • get: corresponds to observing the value for a key (second argument) in a collection (first argument).
  • with: corresponds to observing the right expression using the left expression as the source.
  • has: corresponds to whether the key (second argument) exists within a collection (first argument)
  • mapBlock: the left is the input, the right is an expression to observe on each element of the input.
  • filterBlock: the left is the input, the right is an expression to determine whether the result is included in the output.
  • someBlock: the left is the input, the right is a criterion.
  • everyBlock: the left is the input, the right is a criterion.
  • sortedBlock: the left is the input, the right is a relation on each value of the input on which to compare to determine the order.
  • sortedSetBlock: differs only in semantics from sortedBlock.
  • minBlock: the left is the input, the right is a relation on each value of the input by which to compare the value to others.
  • maxBlock: the left is the input, the right is a relation on each value of the input by which to compare the value to others.
  • groupBlock: the left is the input, the right is an expression that provides the key for an equivalence class for each value in the input. The output is an array of entries, [key, class], with the shared key of every value in the equivalence class.
  • groupMapBlock: has the same input semantics as groupBlock, but the output is a Map instance instead of an array of entries.
  • tuple: has any number of arguments, each an expression to observe in terms of the source value.
  • record: as an args object. The keys are property names for the resulting object, and the values are the corresponding syntax nodes for the values.
  • view: the arguments are the input, the start position, and the length of the sliding window to view from the input. The input may correspond to any ranged content collection, like an array or sorted set.
  • rangeContent: corresponds to the content of an ordered collection that can dispatch indexed range changes like an array or sorted set. This indicates to a binder that it should replace the content of the target instead of replacing the target property with the observed content of the source. A range content node has no effect on the source.
  • mapContent: corresponds to the content of a map-like collection including arrays and all map Collections. These collections dispatch map changes, which create, read, update, or delete key-to-value pairs. This indicates to a binder to replace the content of the target map-like collection with the observed content of the source, instead of replacing the target collection. A map change node on the source side just passes the collection forward without alteration.

For all operators, the "args" property are operands. The node types for unary operators are:

  • +: number, arithmetic coercion
  • -: neg, arithmetic negation
  • !: not, logical negation

For all binary operators, the node types are:

  • **: pow, exponential power
  • //: root, of 2 square root, of 3 cube root, etc
  • %%: log, logarithm with base
  • *: mul, multiplication
  • /: div, division
  • %: mod, modulo (toward negative infinity, always positive)
  • rem: rem, remainder (toward zero, negative if negative)
  • +: add, addition
  • -: sub, subtraction
  • <: lt, less than
  • <=: le, less than or equal
  • >: gt, greater than
  • >=: ge, greater than or equal
  • <=>: compare
  • ==: equals, equality comparison and assignment
  • != produces unary negation and equality comparison or assignment so does not have a corresponding node type. The simplification makes it easier to rotate the syntax tree algebraically.
  • &&, and, logical and
  • ||, or, logical or

For the ternary operator:

  • ? and :: if, ternary conditional

For all function calls, the right hand side is a tuple of arguments.

  • reversed
  • enumerate
  • flatten
  • sum
  • average
  • startsWith
  • endsWith
  • contains
  • join
  • split
  • range
  • keys
  • values
  • entries

Observers and Binders

The observers module contains functions for making all of the different types of observers, and utilities for creating new ones. All of these functions are or return an observer function of the form observe(emit, value, parameters) which in turn returns cancel().

  • observeValue
  • observeParameters
  • makeLiteralObserver(value)
  • makeElementObserver(id)
  • makeComponentObserver(label)
  • makeRelationObserver(callback, thisp) is unavailable through the property binding language, translates a value through a JavaScript function.
  • makeComputerObserver(observeArgs, compute, thisp) applies arguments to the computation function to get a new value.
  • makeConverterObserver(observeValue, convert, thisp) calls the converter function to transform a value to a converted value.
  • makePropertyObserver(observeObject, observeKey)
  • makeGetObserver(observeCollection, observeKey)
  • makeMapFunctionObserver(observeArray, observeFunction)
  • makeMapBlockObserver(observeArray, observeRelation)
  • makeFilterBlockObserver(observeArray, observePredicate)
  • makeSortedBlockObserver(observeArray, observeRelation)
  • makeEnumerationObserver(observeArray)
  • makeFlattenObserver(observeOuterArray)
  • makeTupleObserver(...observers)
  • makeObserversObserver(observers)
  • makeReversedObserver(observeArrayh)
  • makeWindowObserver is not presently available through the language and is subject to change. It is for watching a length from an array starting at an observable index.
  • makeSumObserver(observeArray)
  • makeAverageObserver(observeArray)
  • makeParentObserver(observeExpression)
  • etc

These are utilities for making observer functions.

  • makeNonReplacing(observe) accepts an array observer (the emitted values must be arrays) and returns an array observer that will only emit the target once and then incrementally update that target. All array observers use this decorator to handle the case where the source value gets replaced.
  • makeArrayObserverMaker(setup) generates an observer that uses an array as its source and then incrementally updates a target value, like sum and average. The setup(source, emit) function must return an object of the form {contentChange, cancel} and arrange for emit to be called with new values when contentChange(plus, minus, index) receives incremental updates.
  • makeUniq(callback) wraps an emitter callback such that it only forwards new values. So, if a value is repeated, subsequent calls are ignored.
  • autoCancelPrevious(callback) accepts an observer callback and returns an observer callback. Observer callbacks may return cancelation functions, so this decorator arranges for the previous canceler to be called before producing a new one, and arranges for the last canceler to be called when the whole tree is done.
  • once(callback) accepts a canceler function and ensures that the cancelation routine is only called once.

The binders module contains similar functions for binding an observed value to a bound value. All binders are of the form bind(observeValue, source, target, parameters) and return a cancel() function.

  • makePropertyBinder(observeObject, observeKey)
  • makeGetBinder(observeCollection, observeKey)
  • makeHasBinder(observeCollection, observeValue)
  • makeEqualityBinder(observeLeft, observeRight)
  • makeRangeContentBinder(observeTarget)
  • makeMapContentBinder(observeTarget)
  • makeReversedBinder(observeTarget)
Something went wrong with that request. Please try again.