Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

Binding

Alexander Semenov edited this page Jun 15, 2015 · 19 revisions

Big picture

Binding is a central component of Morearty.js library.

It is responsible for:

  • keeping an application state as an Immutable.js data structure;
  • state modification using a well-defined interface that supports atomicity of changes (i.e. batching of changes), like Clojure's atom on steroids;
  • restricting, or narrowing, visibility to the state's sub-part using sub-bindings which are similar to cursors in some other implementations;
  • providing state change notifications to registered listeners;
  • attaching meta-information to the state's nodes.

How bindings are used in Morearty

To create Morearty context initial state should be passed to Morearty.createContext(...) method. Morearty wraps it into binding and register change listener on it to be notified when re-rendering needs to be performed. This binding is accessible through ctx.getBinding() and points to the state's root. Sub-components receive bindings to the relevant sub-parts of the state by creating sub-bindings using binding.sub('sub.path') method.

Immutability

Bindings are designed to work with Immutable.js. So, every time you get data from bindings, it's represented as primitives or immutable data structures. That means that some Immutable.js API knowledge is required to be able to use bindings effectively. Immutable.js is very powerful, easy to use, and quickly evolving library, see its page for documentation and tutorials.

Working with bindings

Many binding methods accept subpath parameter. It may be specified as a string with . separators or as an array of strings and numbers, e.g. 'path.to.nested.location.1' or ['path', 'to', 'nested', 'location', 1] or even ['path', 'to', 'nested', 'location', '1'] due to Immutable's string indexes auto-coercion. In the examples below single string syntax will be used for better readability.

Bindings define a set of operations on data.

Retrieving data

var immutableValue = binding.get();
// or
var immutableValueAtSubpath = binding.get('sub.path');

Updating data

// directly
binding.set('key', 'value');

// or using a function
binding.update('optional.sub.path', function (immutableValue) {
  return immutableValue.set('key', 'value');
});

Deleting and clearing data

binding.remove('optional.sub.path');
// or
binding.clear('optional.sub.path');

The latter does coll.clear() on nested immutable collection preserving its type.

Merging data

var optionalPreserveConflicting = true;
binding.merge('optional.sub.path', optionalPreserveConflicting, Immutable.Map({ 'key': 'new value' }));

Transactions and chaining

Most of operations about (except get) return this binding and allow chaining. But it's usually required to make a set of changes at once, only notifying listeners at the end (re-rendering once). This functionality reminds database transactions and can be used like this:

binding.atomically().
  set('key', 'new value').
  clear('sub.path').
  update(someAnotherBinding, 'another.sub.path', updateFunction).
  commit();

If you need to make changes silently (skipping listeners notification), pass { notify: false } options object to commit.

Cancelling transactions

Explicit transactions can be cancelled.

If uncommitted transaction is cancelled, further commit will have no effect.

var tx = binding.atomically().set('key', 'new value');
tx.cancel();
tx.commit(); // no effect
tx.isCancelled(); // true

For committed transactions affected paths will be reverted to original values, overwriting any changes made after transaction has been committed.

binding.set('key', 'initial value');
var tx = binding.atomically().set('key', 'new value').commit();
binding.get('key'); // 'new value'
tx.cancel();
tx.isCancelled(); // true
binding.get('key'); // 'initial value'
Implicit transactions

Since version 0.7 Morearty renders asynchronously, i.e. it queues binding updates and renders them in one pass. This provides an implicit transaction and frees from the need to use the above API. Still, it's a good idea to use explicit transactions where appropriate to indicate that a set of changes should be applied atomically.

Using sub-bindings

Sub-bindings are created using sub method:

var subBinding = binding.sub('sub.path');

Updates through sub-binding are visible to parent bindings and are fully synchronized:

subBinding.set('key', 'new value');
binding.get('sub.path.key'); // === 'new value'

There's not much to say about sub-bindings, they are easy and intuitive to use.

Sub-binding are cached

Yes, sub-bindings are cached. If you already requested sub-binding for a sub-path, then it won't be re-created and existing instance is returned. Literally:

binding.sub('sub.path') === binding.sub('sub.path'); // === true
// and moreover
binding.sub('sub.path') === binding.sub('sub').sub('path'); // === true

This is done for performance reasons to minimize objects garbage during render and simplify reasoning.

Listening for changes

Bindings support attaching change listeners:

var listenerId = binding.addListener('optional.sub.path', function (changes) {
  // use changes.getPath/isValueChanged/getPreviousValue/etc methods
  // see API documentation for details
});

Listeners can be removed:

binding.removeListener(listenerId);

or enabled/disabled/disabled during an operation:

binding.disableListener(listenerId);
binding.enableListener(listenerId);
binding.withDisabledListener(listenerId, someOperationFunction);

For easier component lifecycle bound listeners creation see [Listening for state changes](Authoring components#listening-for-state-changes).

Attaching meta information

Meta information can be attached to bindings (i.e. state nodes). This allows to store data you don't want to put in the main state, e.g. validation info, history, and so on. Changes in meta state are considered in render phase.

Meta information is represented as a companion binding for a binding. Access it using meta method like this:

var metaBinding = binding.meta();

and then use like an ordinal binding. You can even attach metadata to metadata, if you like, or use it in transaction with binding it was produced by:

binding.atomically().
  set('key', 'value').
  clear('something').
  update(metaBinding, 'some metadata', updateFunction).
  commit();

The area of possibilities meta-bindings provide is still open, expect Morearty to deeper leverage them in the future. Currently, meta-bindings are used to store history in history module and will very likely be used for validation.

Notice that meta state is not synchronized automatically, e.g. when you delete a state node, its meta value doesn't change. So, you will get same meta value on state node re-creation. This is for simplicity and performance reasons. All synchronization should be done by the user, if required.

Meta state format

Meta binding uses custom format to prevent values interfering between different state nodes:

{
  __meta__: <root meta>,
  foo: {
    __meta__: <foo meta>,
    bar: {
      __meta__: <bar meta>
    }
  },
  baz: {
    __meta__: <baz meta>
  }
}

So, meta state for path x resides at path x.__meta__.

  • Home
  • Essential concepts
    • Binding
    • Context
    • [Asynchronous rendering](Asynchronous rendering)
    • [Authoring components](Authoring components)
  • How to
    • [Bootstrap the app](Bootstrapping the app)
    • [Render on server](Server rendering)
    • [Serialize application state](Serializing application state)
    • [Use history module](Working with history)
Clone this wiki locally