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 A
→ B
:
- The event that will trigger when
A
's state changes - The function to call on/for
A
that returns a representation of the changed state (typically the data that has changed) - The function to call on/for
B
that handles this received data fromA
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
- Development Version (0.5.1) - 12K
- Production Version (0.5.1) - 1.8K minified, gzipped
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 acoerceObject
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 adetectEvent
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 adetectInterface
method or the observer's hook has adetectOtherInterface
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 adetectInterface
method or the subject's hook has adetectOtherInterface
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 callingobserver.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 aSynapse
object is passed, only observing of that object will be stopped.pauseObserving
- same asstopObserving
except event handlers are retained to allow for resuming observing. if aSynapse
object is passed, only observing of that object will be paused.resumeObserving
- all paused obversing is resumed. if aSynapse
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 aSynapse
object is passed, only notifying of that object will be stopped.pauseNotifying
- same asstopNotifying
except event handlers are retained to allow for resuming notifying. if aSynapse
object is passed, only notifying of that object will be paused.resumeNotifying
- all paused notifying is resumed. if an existingSynapse
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 representscheckObjectType
- 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 valuesetHandler
- 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 objectoffEventHandler
- a method which detaches an event handler from an objecttriggerEventHandler
- 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 toSynapse
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 objectdetectOtherInterface
- 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. codejQueryHook.domEvents
- an array of[selector, event]
pairs used bydetectEvent
. codejQueryHook.elementInterfaces
- an array of[selector, interface]
pairs used bydetectInterface
. codejQueryHook.elementBindAttributes
- an array of element attributes used bydetectOtherInterface
. 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
orstopObserving
- 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 usingrequire(['synapse', 'synapse/jquery'], ... );
- Method
addHooks
andclearHooks
have been removed in favor of exposing the rawSynapse.hooks
array. Add or remove hooks by using the native array methods such aspush
andshift
. - 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 compoundclass
interface.
0.4
- Diff- The method
sync
has been renamed tosyncWith
to not conflict with Backbone'sModel.sync
method when the object is augmented.
0.3.2
- Diff- The
css
interface has been renamed toclass
and demoted to a simple interface. As a result, thestyle
interface has been renamed tocss
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