Synapse

Data Binding For The Rest Of Us

Learn

Background

Synapse is a JavaScript data binding library. The API was written to work all-in-code, that is, it does not depend on any template language (e.g. Handlebars) or special attributes (e.g. data-bind) to work.

The API design focus has been placed on making the requirements and options of data binding transparent for developers. There should not be any magic with data binding, since that magic can quickly turn opaque when debugging. Being pragmatic about the design enables Synapse to be a modular foundation for writing additional layers of abstraction on top (such as integration with a template language).

Fundamentally, there are two parties involved: subjects and observers. Keep the following in mind.

Any object can be an observer. It simply waits for the subject's state to change and then does something as a result. In the context of data binding, it may store the subject's state that changed or perform an action as a result of the change.

Subjects on the other hand must have a mechanism for being aware of actions or events that are performed on itself. In the case of the DOM, when a user clicks a button, the onclick event is triggered on that DOM element. These actions can cause a cascade of events all of which can be used to respond to the state changes.

Channels

Synapse provides a mechanism for defining a communication channel between two objects. In order for two objects to communicate, there are three components necessary for AB:

  1. The event that will trigger when A's state changes
  2. The function to call on/for A that returns a representation of the changed state (typically the data that has changed)
  3. The function to call on/for B that handles this received data from A

The channel can be defined with respect to either the subject A or the observer B depending on the system. In either case, whenever a change in state occurs in A, B (and all other observers of A) will be notified.

var A = $('input'),
    B = $('span');

A.bind('keyup', function() {
    B.text(A.val());
});

// Synapse abstracts away the need to write the spaghetti
// code above. The following are functionally equivalent
Synapse(A).notify(B);
Synapse(B).observe(A);

Interfaces

The above examples explain the most simple interactions between two objects. But how is data returned from or sent to each object if they are of different types?

Synapse interfaces provide a way to generically get and set properties on supported objects. A Synapse object has get and set methods which allow for a consistent API regardless of the underlying object type.

The properties that are utilized may or may not cleaning map to the underlying object's properties (or methods). For example, an available interface for the jQuery hook is value. As implied on the right, doing A.set('value', 5) actually calls a.val(5).

// Using Synapse as a constructor exposes the interface
// methods. We are interfacing three different types of
// objects: a DOM element (using the jQuery hook), a Backbone
// model instance and an object primitive.
var a = jQuery('<input value="hello world!">'),
    b = new Backbone.Model(),
    c = {foo: 5};

var A = new Synapse(a),
    B = new Synapse(b),
    C = new Synapse(c);

// These get/set 'interfaces' roughly map the the underlying
// object's API
B.set('message', A.get('value'));
A.set('value', C.get('foo'));
C.set('bar', 10);

b.get('message');   // 'hello world!'
a.val();            // '5'
c.bar;              // 10

Get Started

License

Synapse is released under the BSD 2-clause license. See the LICENSE file for details.

Dependencies

The core Synapse has no dependencies

Download

Right-click and save-as

Hook Name Development Production Dependencies
Object Download Download (none)
jQuery Download Download jQuery
Zepto Download Download Zepto
Backbone Model Download Download Backbone
Backbone View Download Download Backbone

Universal Module Definition (UMD)

Synapse supports the UMD. It can load in the CommonJS/Node environment, via AMD in the browser or as a traditional script.

require(['synapse', 'synapse/jquery'], function(Synapse, jQueryHook) {
    Synapse.hooks.push(jQueryHook);
});

APIs

Constructor

Synapse can be used as a constructor or a function. Using it as a constructor wraps the object and exposes the full Synapse Object API. Using it as a function will augment the passed in object with a limited API.

var object = $('input');
var wrapped = new Synapse(object);
Synapse(object);

Object API

There two primary sets of methods, those that creates a channel between two objects and those that control the channel flow.

Properties

  • raw - a reference to the original object. Note, if the hook this object is used with has a coerceObject method defined, raw will be the object that is returned from that function.
wrapped.raw; // reference to `object`

Channel Methods

  • observe - opens a one-way channel between this object and a subject where this object observes state changes of the subject.
  • notify - opens a one-way channel between this object and an observer where this object notifies it's state changes to the observer.
  • syncWith - opens a two-way channel between this object and another where each acts as a subject and observer.
wrapped.observe(subject, [options]);
wrapped.notify(observer, [options]);
wrapped.syncWith(other);

Channel Configuration

  • event - the event that will trigger the subject to push it's interface value to all observers. this is variable across hooks. this is optional only if the subject's hook has a detectEvent method and can be successfully detected.
  • subjectInterface - the interface identifier of the subject for a given channel or a function. this varies across hooks. this is optional only if the subject's hook has a detectInterface method or the observer's hook has a detectOtherInterface method and can be successfully detected.
  • observerInterface - the interface identifier of the observer for a given channel or a function. this varies across hooks. this is optional only if the observer's hook has a detectInterface method or the subject's hook has a detectOtherInterface method and can be successfully detected.
  • converter - an optional function or method name on the observer that will take the value(s) provided by the subject and return a value prior to calling observer.set(...).
  • triggerOnBind - if true, when the channel is initially created the subject will be triggered using the event supplied or detected.
// available configuration options and their defaults
var options = {
    event: null,
    subjectInterface: null,
    observerInterface: null,
    converter: null,
    triggerOnBind: true
};

// the default, pass in a configuration options object defining
// any or all of the options
wrapped.observe(subject, [options]);

// shorthand method signatures

// no options passed, all `null` options will attempt to be detected
wrapped.observe(subject);

// define the observer and subject interfaces
wrapped.observe(subject, [subjectInterface], [observerInterface]);

// define just a converter
wrapped.observe(subject, [converter]);

Channel Flow Methods

  • stopObserving - stops an object from observing all objects it is currently observing and removes all event handlers referencing this object. if a Synapse object is passed, only observing of that object will be stopped.
  • pauseObserving - same as stopObserving except event handlers are retained to allow for resuming observing. if a Synapse object is passed, only observing of that object will be paused.
  • resumeObserving - all paused obversing is resumed. if a Synapse object is passed, only observing of that object will be resumed.
  • stopNotifying - stops an object from notifying all objects it is currently notifying and removes all event handlers referencing those object. if a Synapse object is passed, only notifying of that object will be stopped.
  • pauseNotifying - same as stopNotifying except event handlers are retained to allow for resuming notifying. if a Synapse object is passed, only notifying of that object will be paused.
  • resumeNotifying - all paused notifying is resumed. if an existing Synapse is object passed, only notifying of that object will be resumed.
wrapped.stopObserving([subject]);
wrapped.pauseObserving([subject]);
wrapped.resumeObserving([subject]);
wrapped.stopNotifying([observer]);
wrapped.pauseNotifying([observer]);
wrapped.resumeNotifying([observer]);

Interface Methods

  • get - returns a representation of the object's data for the given interface.
  • set - sets a value relative to the given interface for the object.
wrapped.get(interface);
wrapped.set(interface, value);

Hook API

Synapse relies on hooks being defined to perform the under-the-hood operations. The Synapse constructor is simply an abstraction for these hooks which are defined on a per-object-type basis. Depending on the specificity of the type, objects can fallback to less specific hooks to have at least minimal functionality.

Base

A hook is simply an object. To make it distributable, it is recommended to define it as a standalone module. The base parts of a hook include.

  • typeName - name of the object type this hook represents
  • checkObjectType - a method which checks if the object is applicable for this hook
define(function() {
    return {
        // the name of the type this hook represents
        typeName: 'The Awesome Type',

        // checks whether the object is the correct type for this hook
        checkObjectType: function(object) {},

Interfaces

The following methods are required for an object to an observer.

  • getHandler - a method which takes an object and an interface identifier and returns a value
  • setHandler - a method which takes an object, an interface identifier, and a value and sets the value
        ...
        // takes an object and a interface identifier and returns a value
        getHandler: function(object, interface) {},

        // takes an object, inteface identifier and value and sets the value
        setHandler: function(object, interface, value) {},
        ...

Events

The following methods are required for an object to a subject.

  • onEventHandler - a method which attaches an event handler to an object
  • offEventHandler - a method which detaches an event handler from an object
  • triggerEventHandler - a method which triggers and event on an object
        ...
        // takes an object, event and handler and attaches the handler
        onEventHandler: function(object, event, handler) {},

        // take an object, event and handler and detaches the handler
        offEventHandler: function(object, event, handler) {},

        // takes an object and triggers an event on it
        triggerEventHandler: function(object, event) {},
        ...

Helpers

The following methods are optional, but may be useful.

  • coerceObject - takes the raw object passed in to Synapse and coerces the object into a more useful object (think CSS selector → jQuery object)
        ...
        // takes the raw object passed in to Synapse and coerces the object
        // into a more useful object (think CSS selector --> jQuery object)
        coerceObject: function(object) {},
        ...

Detection

These methods add much of desired magic for automatic binding between objects.

  • detectInterface - detects an appropriate interface for the given object
  • detectOtherInterface - detects an appropriate interface for the other object involved (either subject or observer)
  • detectEvent - (subjects only) detects an appropriate event for the given object which will trigger the downstream notification to observers
        ...
        // detects an appropriate interface for the given object
        detectInterface: function(object) {},

        // detects an appropriate interface for the other object involved
        // (either subject or observer)
        detectOtherInterface: function(object) {},

        // detects an appropriate event for the given object which will
        // trigger the downstream notification to observers
        detectEvent: function(object, [interface]) {}
    };
});

Supported Hooks

Plain Objects

Supported Types Plain objects are typically just object primitives, e.g. {}, or objects created from the Object constructor, e.g. new Object().
Events These objects are assumed to not have support for events and thus can only be observers. As a result, using this hook alone is pretty useless since it has nothing to observe.. it is a lonely object.
Interfaces The interfaces available for plain objects are simply, their properties and methods. Methods always take precedence over properties.

Sanitize Plain Objects

Synapse uses an internal expando property to keep track of all managed objects to prevent defining duplicate channels. This means plain objects will obtain a property that looks something like Synapse0508635553480125964. It may be desirable for downstream processing to remove this property. Synapse.expando contains the property name so the property can be removed.

var obj = {};
// wrapped, expando property is added to the object
var wrapped = new Synapse(obj);
console.log(obj);
{
    Synapse0508635553480125964: 29
}

// The expando property ensures if the object gets wrapped again,
// it won't created a new object
var wrapped2 = new Synapse(obj);
wrapped2 === wrapped; // true

// It is recommended to create a copy of the object to perform
// post-processing (delete the expando property)

// using Underscore
var clone = _.clone(obj);
// or using jQuery
var clone = $.extend({}, obj);

delete clone[Synapse.expando];

// do downstream processing with the clone..

jQuery

Supported Types jQuery objects, CSS selectors, and DOM elements.
Events DOM events via jQuery's on, off, and trigger methods
Interfaces jQuery provides a very nice API which encapsulates cross-browser inconsistencies with a few other niceties. Full list below.

Hook Extensions

In addition to implementing the Hook API discussed above, the jQuery hook exposes additional configurable components for customizing the default behavior when interfacing with jQuery objects.

  • jQueryHook.interfaces - contains the registry of all available interfaces for jQuery objects. code
  • jQueryHook.domEvents - an array of [selector, event] pairs used by detectEvent. code
  • jQueryHook.elementInterfaces - an array of [selector, interface] pairs used by detectInterface. code
  • jQueryHook.elementBindAttributes - an array of element attributes used by detectOtherInterface. code
define(['synapse/jquery'], function(jQueryHook) {

    // The interface registry object
    jQueryHook.interfaces.registry

    // Register a new interface (name must be unique)
    jQueryHook.interfaces.register({
        name: 'awesomness',
        get: function() { ... },
        set: function() { ... }
    });

    // Unregister interface
    jQueryHook.interfaces.unregister('awesomness');
});

Simple Interfaces

Most of the simple interfaces are value-based or toggle-like properties.

text
html
value
enabled
disabled
checked
visible
hidden

Compound Interfaces

Compound intefaces map more directly to the underlying object API and exposes more granular control.

Compound interface identifiers are delimited by a period (.). The first key is the reference interface and typically maps to a method on the object in which it itself takes a key (thus being compound).

prop.value
attr.href Variable Link
css.color Color
data.role Nothing to show, but for the curious open up the console and type document.ex3.data('role')
class.stop DON'T ENTER A VALUE!

Zepto

Supported Types Zepto arrays, CSS selectors, and DOM elements.
Events DOM events via Zepto's bind, unbind, and trigger methods
Interfaces The Zepto hook implements the same interfaces as the jQuery hook with the exception of the prop.<key> compound interface since Zepto does not differentiate between properties and attributes in the API.

Backbone View

Supported Types Backbone.View objects
Events Similar to the jQuery hook under the hood, therefore using the DOM events mentioned above.
Interfaces Similar to the jQuery hook under the hood, therefore all interfaces above work for these objects. In addition, interface identifier that reference a method name on the Backbone.View object directly may be used. Note, this will take precedence over the above jQuery interfaces of the same name.

Backbone Model

Supported Types Backbone.Model objects
Events Uses the built-in event system Backbone provides
Interfaces Similar to the plain objects above, the interface identifiers correspond to properties and methods. Under the hood, getting and setting the properties will be done via the object's native get and set methods (this ensures when a property is set, the change events fire).

Change Log

0.5.1

- Diff
  • Fix clean up of channels when either stopNotifying or stopObserving
  • Allow functions to be passed in as interfaces
  • Converter functions are now passed the value, the observer and the observer's interface

0.5

- Diff
  • Remove hooks directory, so hooks can be required using require(['synapse', 'synapse/jquery'], ... );
  • Method addHooks and clearHooks have been removed in favor of exposing the raw Synapse.hooks array. Add or remove hooks by using the native array methods such as push and shift.
  • Add expando property to prevent duplication of channels between objects. This will reduce the number of objects initialized. The passed in object will receive a property that looks like Synapse0508635553480125964. Caution should be taken when using Synapse with plain objects to prevent exposing this property during downstream processing. See the above warning.
  • Refresh documentation, simplify style.

0.4.2

- Diff
  • The simple class interface has been removed in favor of an existential compound class interface.

0.4

- Diff
  • The method sync has been renamed to syncWith to not conflict with Backbone's Model.sync method when the object is augmented.

0.3.2

- Diff
  • The css interface has been renamed to class and demoted to a simple interface. As a result, the style interface has been renamed to css for consistency with jQuery and Zepto.

0.3

- Diff
  • Breaking change, the delimiter between compond interface keys is now a period (.) rather than a colon (:). This mentally maps better to it's multi-method/property traversal on the object.

0.2

- Diff
  • Breaking change, the delimiter between compond interface keys is now a period (.) rather than a colon (:). This mentally maps better to it's multi-method/property traversal on the object.

0.1

- Diff
  • Initial release