-
Notifications
You must be signed in to change notification settings - Fork 41
Binding
- Big picture
- How bindings are used in Morearty
- Immutability
- Working with bindings
- Using sub-bindings
- Listening for changes
- Attaching meta information
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.
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.
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.
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.
var immutableValue = binding.get();
// or
var immutableValueAtSubpath = binding.get('sub.path');
// directly
binding.set('key', 'value');
// or using a function
binding.update('optional.sub.path', function (immutableValue) {
return immutableValue.set('key', 'value');
});
binding.remove('optional.sub.path');
// or
binding.clear('optional.sub.path');
The latter does coll.clear()
on nested immutable collection preserving its type.
var optionalPreserveConflicting = true;
binding.merge('optional.sub.path', optionalPreserveConflicting, Immutable.Map({ 'key': 'new value' }));
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
.
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'
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.
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.
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.
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).
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 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__
.
Found a spelling or grammar error on this page? Have an idea of how to improve the text? File an issue to help us to make Morearty.js better.
- Home
- Essential concepts
- 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)