Skip to content

Instantly share code, notes, and snippets.

@willbailey
Created October 21, 2011 01:31
Show Gist options
  • Save willbailey/1302885 to your computer and use it in GitHub Desktop.
Save willbailey/1302885 to your computer and use it in GitHub Desktop.
connections
// ### Events
// Event mixin copied from Backbone
var Events = {
bind: function(evt, callback) {
var events = this._events = this._events || {};
var callbacks = events[evt] = events[evt] || [];
callbacks.push(callback);
},
unbind: function(evt, callback) {
if (!this._events) return;
if (!evt && !callback) {
this._events = {};
} else if (!callback) {
delete this._events[evt];
} else {
var callbacks = this._events[evt] || [];
for (var i = 0, len = callbacks.length; i < len; i++) {
callback === callbacks[i] && callbacks.splice(i,1);
}
}
},
trigger: function(evt) {
var i, len, events = this._events || {},
callbacks = events[evt] = events[evt] || [];
for (i = 0, len = callbacks.length; i < len; i++) {
callbacks[i].apply(this, Array.prototype.slice.call(arguments, 1));
}
if (!events['all']) return;
callbacks = events['all'];
for (i = 0, len = callbacks.length; i < len; i++) {
callbacks[i].apply(this, Array.prototype.slice.call(arguments));
}
}
};
// ### Utilities
// Extend an object with properties of the passed in object[s]
var extend = function(obj) {
var extensions = Array.prototype.slice.call(arguments, 1);
for (var i = 0, len = extensions.length; i < len; i++) {
var extension = extensions[i];
for (var key in extension) {
if (extension.hasOwnProperty(key)) {
obj[key] = extension[key];
}
}
}
};
// bind a function to an execution context
// TODO: use native bind if possible
var bind = function(func, context) {
var args = Array.prototype.splice.call(arguments, 2);
return function() {
func.apply(context, args.concat(Array.prototype.slice.call(arguments)));
};
};
// TODO: detect if an object is a DOM node
var isElement = function(obj) {
return (
typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
typeof o === "object" && o.nodeType === 1 && typeof o.nodeName==="string");
};
// ### Connectable wraps an object so it can be connected
// You can specify a list of transforms per property to apply
// in the format:
// {
// property: function(inValue) {return inValue.toLowerCase()}
// }
// You can also specify a list of before and after hooks to be fired
// before and after the property is changed.
Connectable = function(obj, options) {
this.obj = isElement(obj) ? this.nodeProxy(obj) : obj;
this._transforms = options.transforms || {};
this._befores = options.befores || {};
this._afters = options.afters || {};
};
extend(Connectable.prototype, Events, {
// TODO: if we get a dom node we'll create a proxy for it that observers the
// DOM events it fires and updates a proxy object that can be connected
// directly.
nodeProxy: function(element) {
// TODO: observe dom node for state changes
var listener = function() {
};
element.addEventListener('keypress', bind(listener, this));
element.addEventListener('change', bind(listener, this));
return obj;
},
// Update a property of the object and fire an event to notify any connections
// If the silent flag is passed, no event is fired.
set: function(key, value, silent) {
var priorValue = this.obj[key];
if (priorValue !== value && !silent) {
this._beforeChange();
this.obj[key] = this._transform(value, key);
this._afterChange();
this.trigger('change', {property: key, priorValue: priorValue});
}
},
// Get a property of the wrapped object.
get: function(key) {
return this.obj[key];
},
// Invoke any transforms on the changed key
_transform: function(value, key) {
var transformer = this._transformers[key];
if (transformer) {
return transformer(value);
} else {
return value;
}
},
// fire before change hooks
_beforeChange: function(value, key) {
this._executeHooks(value, key, this._befores);
},
// fire after change hooks
_afterChange: function(value, key) {
this._executeHooks(value, key, this._afters);
},
_executeHooks: function(value, key, hooks) {
var keyHook = hooks[key];
var allHook = hooks['all'];
keyHook && keyHook(value);
allHook && allHook(value, key);
}
});
// ### Connection
// The connection class maintains a link between two objects referred to as
// the left and right object. When a change event fires on one of the connected
// objects the connection syncs the state on the connected objects.
//
// The objects passed into the connection are wrapped in the Connectable
// interface to facilitate the generation of events and synchronization of
// state.
Connection = function(left, right) {
this.left = left.isConnectable ? left : new Connectable(left);
this.right = right.isConnectable ? right : new Connectable(right);
this.left.bind('change', bind(this.leftChange, this));
this.right.bind('change', bind(this.rightChange, this));
};
extend(Connection.prototype, Events, {
leftChange: function(data) {
this.sync(this.right, this.left, data.property);
},
rightChange: function(data) {
this.sync(this.left, this.right, data.property);
},
sync: function(target, source, property) {
target.set(property, source.get(property), true);
}
});
// TODO: deal with collections and arrays
@kassens
Copy link

kassens commented Oct 21, 2011

I don't know if you intend to use this, but while reading it I noticed two things:

Line 19 has a subtle bug (at least I think it does): If two same callbacks are in the array one after the other only the first will be deleted, just use for (var i = array.length; i--; ) { ... } instead.
Line 28 and 33: the result of Array.prototype.slice.call can be cached.
Line

@willbailey
Copy link
Author

Thanks Jan!...was just a sketch of an idea I had for data binding I was showing Jordan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment