Last active
August 29, 2015 13:59
-
-
Save richardbutler/eb197c28f9c5a3c275ff to your computer and use it in GitHub Desktop.
Catalyst - data binding for React
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//------------------------------------------------------------------------------ | |
// | |
// Dependencies | |
// | |
//------------------------------------------------------------------------------ | |
var Emitter = require('component-emitter'); | |
var Bindable = require('./Bindable'); | |
//------------------------------------------------------------------------------ | |
// | |
// Constructor | |
// | |
//------------------------------------------------------------------------------ | |
function ArrayList(source) { | |
this.reset(source); | |
} | |
//------------------------------------------------------------------------------ | |
// | |
// Static methods | |
// | |
//------------------------------------------------------------------------------ | |
ArrayList.isArrayList = function(value) { | |
return value instanceof ArrayList; | |
}; | |
//------------------------------------------------------------------------------ | |
// | |
// Events | |
// | |
//------------------------------------------------------------------------------ | |
/** | |
* Event: any change happened - add, remove, reset, refresh, update. | |
* @type {string} | |
*/ | |
ArrayList.CHANGE = 'change'; | |
/** | |
* Event: an item, or collection of items, was added. | |
* @type {string} | |
*/ | |
ArrayList.ADD = 'add'; | |
/** | |
* Event: an item, or collection of items, was removed. | |
* @type {string} | |
*/ | |
ArrayList.REMOVE = 'remove'; | |
/** | |
* Event: the collection was reset with new data. | |
* @type {string} | |
*/ | |
ArrayList.RESET = 'reset'; | |
/** | |
* Event: an item within the collection was changed in some way. | |
* @type {string} | |
*/ | |
ArrayList.UPDATE = 'update'; | |
//------------------------------------------------------------------------------ | |
// | |
// ArrayList prototype | |
// | |
//------------------------------------------------------------------------------ | |
ArrayList.prototype = Object.create(null, { | |
//-------------------------------------------------------------------------- | |
// | |
// Public properties | |
// | |
//-------------------------------------------------------------------------- | |
source: { | |
writable: true, | |
enumerable: true | |
}, | |
//-------------------------------------------------------------------------- | |
// | |
// Public methods | |
// | |
//-------------------------------------------------------------------------- | |
add: { | |
writable: false, | |
value: function(items, index) { | |
var list = this; | |
items = toArray(items); | |
items | |
.filter(Bindable.isBindable) | |
.forEach(function(item) { | |
item.on(Bindable.CHANGE, list.onItemChange, list); | |
}); | |
var args = [typeof index === 'undefined' ? this.source.length : index, 0].concat(items); | |
this.source.splice.apply(this.source, args); | |
this.emitChange({ | |
type: ArrayList.ADD, | |
items: items | |
}); | |
} | |
}, | |
push: { | |
writable: false, | |
value: function() { | |
if (arguments.length) { | |
this.add([].slice.call(arguments)); | |
} | |
} | |
}, | |
unshift: { | |
writable: false, | |
value: function() { | |
if (arguments.length) { | |
this.add([].slice.call(arguments), 0); | |
} | |
} | |
}, | |
remove: { | |
writable: false, | |
value: function(items) { | |
var list = this; | |
var source = this.source; | |
items = toArray(items).slice(); | |
items.forEach(function(item) { | |
if (Bindable.isBindable(item)) { | |
item.off(Bindable.CHANGE, list.onItemChange, list); | |
} | |
source.splice(source.indexOf(item), 1); | |
}); | |
this.emitChange({ | |
type: ArrayList.REMOVE, | |
items: items | |
}); | |
} | |
}, | |
pop: { | |
writable: false, | |
value: function() { | |
var item; | |
if (this.length) { | |
item = this.last(); | |
this.remove(item); | |
} | |
return item; | |
} | |
}, | |
shift: { | |
writable: false, | |
value: function() { | |
var item; | |
if (this.length) { | |
item = this.first(); | |
this.remove(item); | |
} | |
return item; | |
} | |
}, | |
removeAll: { | |
writable: false, | |
value: function() { | |
this.remove(this.source); | |
} | |
}, | |
reset: { | |
writable: false, | |
value: function(items) { | |
items = toArray(items); | |
this.source = items.slice(); | |
this.emitChange({ type: ArrayList.RESET }); | |
} | |
}, | |
at: { | |
writable: false, | |
value: function(index) { | |
return this.source[index]; | |
} | |
}, | |
first: { | |
writable: false, | |
value: function() { | |
return this.at(0); | |
} | |
}, | |
last: { | |
writable: false, | |
value: function() { | |
return this.length ? this.at(this.length - 1) : undefined; | |
} | |
}, | |
contains: { | |
writable: false, | |
value: function(item) { | |
return this.indexOf(item) >= 0; | |
} | |
}, | |
indexOf: { | |
writable: false, | |
value: function(item) { | |
return this.source.indexOf(item); | |
} | |
}, | |
length: { | |
get: function() { | |
return this.source ? this.source.length : 0; | |
} | |
}, | |
toArray: { | |
writable: false, | |
value: function() { | |
return this.source.slice(); | |
} | |
}, | |
emitChange: { | |
writable: false, | |
value: function(event) { | |
this.emit(event); | |
this.emit({ | |
type: ArrayList.CHANGE, | |
relatedEvent: event | |
}); | |
} | |
}, | |
//-------------------------------------------------------------------------- | |
// | |
// Event handlers | |
// | |
//-------------------------------------------------------------------------- | |
onItemChange: { | |
writable: false, | |
value: function(event) { | |
this.emit({ | |
type: ArrayList.UPDATE, | |
relatedEvent: event | |
}); | |
this.emit({ | |
type: ArrayList.CHANGE, | |
relatedEvent: event | |
}); | |
} | |
} | |
}); | |
function proxyMethod(name) { | |
Object.defineProperty(ArrayList.prototype, name, { | |
writable: false, | |
value: function() { | |
return Array.prototype[name].apply(this.source, arguments); | |
} | |
}); | |
} | |
// Proxy methods to underlying source array | |
['forEach', 'map', 'filter', 'reduce', 'join'].forEach(proxyMethod); | |
// Mixin the Emitter | |
Emitter(ArrayList.prototype); | |
// Seal the prototype | |
Object.seal(ArrayList.prototype); | |
function toArray(items) { | |
if (!items) return []; | |
return Array.isArray(items) ? items : [items]; | |
} | |
module.exports = ArrayList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var chai = require('chai'); | |
var expect = chai.expect; | |
var sinon = require('sinon'); | |
var C = require('..'); | |
chai.config.includeStack = true; | |
describe('ArrayList', function() { | |
describe('bindings', function() { | |
it('should detect simple changes', function() { | |
var Model = C.Bindable.define({ | |
name: { writable: true } | |
}); | |
var foo = new Model({ name: 'foo' }); | |
var bar = new Model({ name: 'bar' }); | |
var baz = new Model({ name: 'baz' }); | |
var list = new C.ArrayList([foo, bar]); | |
var add = sinon.spy(); | |
var change = sinon.spy(); | |
var update = sinon.spy(); | |
expect(list.at(0)).to.equal(foo); | |
expect(list.at(1)).to.equal(bar); | |
expect(list.length).to.equal(2); | |
list.on(C.ArrayList.ADD, add); | |
list.on(C.ArrayList.CHANGE, change); | |
list.on(C.ArrayList.UPDATE, update); | |
list.add(baz); | |
expect(list.length).to.equal(3); | |
baz.name = 'BAZ'; | |
sinon.assert.calledOnce(add); | |
sinon.assert.calledWith(add); | |
sinon.assert.calledTwice(change); | |
sinon.assert.calledOnce(update); | |
}); | |
}); | |
describe('methods', function() { | |
var empty, list, add, remove, change, reset, refresh; | |
beforeEach(function() { | |
add = sinon.spy(); | |
remove = sinon.spy(); | |
change = sinon.spy(); | |
reset = sinon.spy(); | |
refresh = sinon.spy(); | |
empty = new C.ArrayList(); | |
list = new C.ArrayList(['one', 'two', 'three']); | |
list.on(C.ArrayList.ADD, add); | |
list.on(C.ArrayList.REMOVE, remove); | |
list.on(C.ArrayList.CHANGE, change); | |
list.on(C.ArrayList.RESET, reset); | |
list.on(C.ArrayList.REFRESH, refresh); | |
}); | |
it('first', function() { | |
expect(empty.first()).to.be.undefined; | |
expect(list.first()).to.equal('one'); | |
}); | |
it('last', function() { | |
expect(empty.last()).to.be.undefined; | |
expect(list.last()).to.equal('three'); | |
}); | |
it('filter', function() { | |
expect(list.filter(function(d) { return d === 'two' })[0]).to.equal('two'); | |
}); | |
it('length', function() { | |
expect(empty.length).to.equal(0); | |
expect(list.length).to.equal(3); | |
}); | |
it('contains', function() { | |
expect(empty.contains('foo')).to.be.false; | |
expect(list.contains('one')).to.be.true; | |
expect(list.contains('ONE')).to.be.false; | |
}); | |
it('at', function() { | |
expect(empty.at(17)).to.be.undefined; | |
expect(list.at(0)).to.equal('one'); | |
expect(list.at(2)).to.equal('three'); | |
}); | |
describe('add', function() { | |
var items = ['four', 'five']; | |
it('to end', function() { | |
list.add(items); | |
expect(list.length).to.equal(5); | |
sinon.assert.calledWith(add, { | |
type: C.ArrayList.ADD, | |
items: items, | |
target: list | |
}); | |
sinon.assert.calledOnce(change); | |
}); | |
it('at index', function() { | |
list.add(items, 1); | |
expect(list.join(',')).to.equal(['one', 'four', 'five', 'two', 'three'].join(',')); | |
sinon.assert.calledWith(add, { | |
type: C.ArrayList.ADD, | |
items: items, | |
target: list | |
}); | |
sinon.assert.calledOnce(change); | |
}); | |
}); | |
it('remove', function() { | |
var items = ['four', 'five']; | |
list.add(items); | |
expect(list.length).to.equal(5); | |
sinon.assert.calledWith(add, { | |
type: C.ArrayList.ADD, | |
items: items, | |
target: list | |
}); | |
sinon.assert.calledOnce(change); | |
}); | |
it('removeAll', function() { | |
var items = list.toArray(); | |
list.removeAll(); | |
expect(list.length).to.equal(0); | |
sinon.assert.calledWith(remove, { | |
type: C.ArrayList.REMOVE, | |
items: items, | |
target: list | |
}); | |
sinon.assert.calledOnce(change); | |
}); | |
it('push', function() { | |
list.push('four', 'five'); | |
expect(list.length).to.equal(5); | |
expect(list.last()).to.equal('five'); | |
sinon.assert.calledOnce(add); | |
sinon.assert.calledOnce(change); | |
}); | |
it('pop', function() { | |
expect(list.pop()).to.equal('three'); | |
expect(list.join(',')).to.equal(['one', 'two'].join(',')); | |
sinon.assert.calledOnce(remove); | |
sinon.assert.calledOnce(change); | |
}); | |
it('shift', function() { | |
expect(list.shift()).to.equal('one'); | |
expect(list.join(',')).to.equal(['two', 'three'].join(',')); | |
sinon.assert.calledOnce(remove); | |
sinon.assert.calledOnce(change); | |
}); | |
it('unshift', function() { | |
list.unshift('minusone', 'zero'); | |
expect(list.length).to.equal(5); | |
expect(list.first()).to.equal('minusone'); | |
expect(list.last()).to.equal('three'); | |
sinon.assert.calledOnce(add); | |
sinon.assert.calledOnce(change); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Emitter = require('component-emitter'); | |
var ClassRegistry = require('./ClassRegistry'); | |
var proto = { | |
eventsSuspended: { | |
value: false, | |
enumerable: true | |
}, | |
emitChange: { | |
value: function(property, newValue, oldValue) { | |
if (this.eventsSuspended || oldValue === newValue) return; | |
this.emit({ | |
type: Bindable.CHANGE, | |
property: property, | |
newValue: newValue, | |
oldValue: oldValue | |
}); | |
}, | |
enumerable: true | |
}, | |
watch: { | |
value: function(property, handler, context) { | |
this.on(Bindable.CHANGE, function(event) { | |
if (event.property === property) { | |
handler.apply(context || this, arguments); | |
} | |
}, context); | |
}, | |
enumerable: true | |
}, | |
onChildChange: { | |
value: function(event) { | |
var model = this; | |
var property = Object.keys(this.__values__).filter(function(key) { | |
return event.target === model[key]; | |
})[0]; | |
if (event.property) { | |
property += '.' + event.property; | |
} | |
this.emit({ | |
type: Bindable.CHANGE, | |
property: property, | |
relatedEvent: event | |
}); | |
}, | |
enumerable: true | |
} | |
}; | |
var Bindable = { | |
CHANGE: 'change', | |
isBindable: function(obj) { | |
// Duck typing | |
return obj && (typeof obj.__values__ !== 'undefined'); | |
}, | |
fromObject: function(values) { | |
var config = Object.keys(values).reduce(function(config, key) { | |
config[key] = { enumerable: true, writable: true, defaultValue: values[key] }; | |
return config; | |
}, {}); | |
var Model = Bindable.define(config); | |
return new Model(values); | |
}, | |
define: function() { | |
var className = arguments.length >= 2 ? arguments[0] : undefined; | |
var schema = arguments[arguments.length - 1]; | |
// Collate default values | |
var defaults = Object.keys(schema).reduce(function(defaults, key) { | |
defaults[key] = schema[key].defaultValue; | |
return defaults; | |
}, {}); | |
function ObjectProxy(attributes) { | |
var bindable = this; | |
Object.defineProperty(this, '__values__', { | |
writable: false, | |
enumerable: true, | |
value: {} | |
}); | |
// Suspend events | |
this.eventsSuspended = true; | |
function assign(attributes) { | |
if (typeof attributes === 'object') { | |
for (var key in attributes) { | |
if (attributes.hasOwnProperty(key)) { | |
bindable[key] = attributes[key]; | |
} | |
} | |
} | |
} | |
// Set default values, without triggering events | |
assign(defaults); | |
// Set passed initial values, without triggering events | |
assign(attributes); | |
// Events are okay again | |
this.eventsSuspended = false; | |
} | |
// Set constructor | |
ObjectProxy.prototype.constructor = ObjectProxy; | |
// Mixin the Emitter | |
Emitter(ObjectProxy.prototype); | |
// Create additional properties | |
Object.defineProperties(ObjectProxy.prototype, proto); | |
// Attach event triggers to getters and setters | |
Object.keys(schema).forEach(function(key) { | |
var keyConfig = schema[key]; | |
Bindable.defineProperty(ObjectProxy.prototype, key, keyConfig); | |
}); | |
// Seal the prototype | |
Object.seal(ObjectProxy.prototype); | |
// Assign className, if passed | |
if (className) { | |
ObjectProxy.className = className; | |
ClassRegistry.register(className, ObjectProxy); | |
} | |
return ObjectProxy; | |
}, | |
defineProperty: function(obj, key, keyConfig) { | |
// Don't rewrite read-only properties | |
if (keyConfig.writable === false) return; | |
// If we're writable, remove the key as we're rewriting the setter | |
delete keyConfig.writable; | |
var setter = keyConfig['set']; | |
keyConfig['set'] = function(value) { | |
var oldValue = this[key]; | |
if (isBindableObject(oldValue)) { | |
oldValue.off(Bindable.CHANGE, this.onChildChange, this); | |
} | |
if (typeof setter === 'function') { | |
setter.apply(this, arguments); | |
} else { | |
this.__values__[key] = value; | |
} | |
if (isBindableObject(value)) { | |
value.on(Bindable.CHANGE, this.onChildChange, this); | |
} | |
this.emitChange(key, value, oldValue); | |
}; | |
keyConfig['get'] = keyConfig['get'] || function() { | |
return this.__values__[key]; | |
}; | |
// Kill the default value and assign it as a default | |
if (keyConfig.value) { | |
// We're creating a property at define-time | |
keyConfig.defaultValue = keyConfig.value; | |
} | |
delete keyConfig.value; | |
// Define property from config | |
Object.defineProperty(obj, key, keyConfig); | |
if (obj.__values__) { | |
// We're adding a dynamic property at runtime | |
obj[key] = keyConfig.defaultValue; | |
} | |
} | |
}; | |
/** | |
* Naively check if an object is bindable - for now, just duck-typing against | |
* event dispatching capabilities is enough. | |
* | |
* @param {object} value | |
* @return {boolean} | |
*/ | |
function isBindableObject(value) { | |
return value && typeof value.on === 'function'; | |
} | |
module.exports = Bindable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var chai = require('chai'); | |
var expect = chai.expect; | |
var sinon = require('sinon'); | |
var _ = require('lodash'); | |
var Bindable = require('..').Bindable; | |
var ClassRegistry = require('..').ClassRegistry; | |
chai.config.includeStack = true; | |
describe('Bindable', function() { | |
afterEach(function() { | |
ClassRegistry.clear(); | |
}); | |
describe('simple binding', function() { | |
var Model; | |
var model; | |
beforeEach(function() { | |
Model = Bindable.define('Model', { | |
foo: { writable: true } | |
}); | |
model = new Model({ foo: 'bar' }); | |
}); | |
it('should add the class to the registry', function() { | |
expect(Model.className).to.equal('Model'); | |
expect(ClassRegistry.getClass('Model')).to.equal(Model); | |
}); | |
it('should detect simple changes', function() { | |
var change = sinon.spy(); | |
expect(model.foo).to.equal('bar'); | |
var event = { | |
type: Bindable.CHANGE, | |
property: 'foo', | |
newValue: 'baz', | |
oldValue: 'bar', | |
target: model | |
}; | |
model.on(Bindable.CHANGE, change); | |
model.foo = 'baz'; | |
sinon.assert.calledWith(change, event); | |
}); | |
it('shouldn\'t fire if the value is the same', function() { | |
var changeFoo = sinon.spy(); | |
expect(model.foo).to.equal('bar'); | |
model.on(Bindable.CHANGE, changeFoo); | |
model.foo = 'baz'; | |
expect(model.foo).to.equal('baz'); | |
sinon.assert.calledOnce(changeFoo); | |
model.foo = 'baz'; | |
sinon.assert.calledOnce(changeFoo); | |
}); | |
}); | |
describe('child binding', function() { | |
var User; | |
var model; | |
var john, paul; | |
beforeEach(function() { | |
User = Bindable.define({ | |
name: { defaultValue: 'John' }, | |
age: { defaultValue: 30 } | |
}); | |
john = new User(); | |
paul = new User({ name: 'Paul', age: 35 }); | |
model = Bindable.fromObject({ | |
user: john | |
}); | |
}); | |
it('should remove event handlers when another value is assigned', function() { | |
expect(john.callbacks['change']).to.be.an('array'); | |
expect(john.callbacks['change'].length).to.equal(1); | |
model.user = paul; | |
expect(john.callbacks['change'].length).to.equal(0); | |
expect(paul.callbacks['change']).to.be.an('array'); | |
expect(paul.callbacks['change'].length).to.equal(1); | |
}); | |
it('should receive events from child objects', function() { | |
var change = sinon.spy(); | |
model.on(Bindable.CHANGE, change); | |
model.user.age = 32; | |
sinon.assert.calledOnce(change); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Bindable = require('./Bindable'); | |
var types = {}; | |
var ClassRegistry = { | |
createByType: function(type, options) { | |
var cls = this.getClass(type); | |
return new cls(options); | |
}, | |
register: function(type, cls) { | |
types[type.toLowerCase()] = cls; | |
}, | |
getClass: function(type) { | |
return types[type.toLowerCase()]; | |
}, | |
clear: function() { | |
types = {}; | |
} | |
}; | |
module.exports = ClassRegistry; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var chai = require('chai'); | |
var expect = chai.expect; | |
var ClassRegistry = require('..').ClassRegistry; | |
describe('ClassRegistry', function() { | |
afterEach(function() { | |
ClassRegistry.clear(); | |
}); | |
it('should register a class', function() { | |
var Model = {}; | |
ClassRegistry.register('Model', Model); | |
expect(ClassRegistry.getClass('Model')).to.equal(Model); | |
expect(ClassRegistry.getClass('model')).to.equal(Model); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Expose `Emitter`. | |
*/ | |
module.exports = Emitter; | |
/** | |
* Initialize a new `Emitter`. | |
* | |
* @api public | |
*/ | |
function Emitter(obj) { | |
if (obj) return mixin(obj); | |
} | |
/** | |
* Mixin the emitter properties. | |
* | |
* @param {Object} obj | |
* @return {Object} | |
* @api private | |
*/ | |
function mixin(obj) { | |
for (var key in Emitter.prototype) { | |
obj[key] = Emitter.prototype[key]; | |
} | |
return obj; | |
} | |
/** | |
* Listen on the given `event` with `fn`. | |
* | |
* @param {String} event | |
* @param {Function} fn | |
* @param {Object} [context] | |
* @return {Emitter} | |
* @api public | |
*/ | |
Emitter.prototype.on = | |
Emitter.prototype.addEventListener = function(event, fn, context){ | |
this.callbacks = this.callbacks || {}; | |
(this.callbacks[event] = this.callbacks[event] || []) | |
.push({ fn: fn, context: context }); | |
return this; | |
}; | |
/** | |
* Adds an `event` listener that will be invoked a single | |
* time then automatically removed. | |
* | |
* @param {String} event | |
* @param {Function} fn | |
* @param {Object} [context] | |
* @return {Emitter} | |
* @api public | |
*/ | |
Emitter.prototype.once = function(event, fn, context){ | |
var self = this; | |
this.callbacks = this.callbacks || {}; | |
function on() { | |
self.off(event, on); | |
fn.apply(context || this, arguments); | |
} | |
on.fn = fn; | |
this.on(event, on); | |
return this; | |
}; | |
/** | |
* Remove the given callback for `event` or all | |
* registered callbacks. | |
* | |
* @param {String} event | |
* @param {Function} fn | |
* @param {Object} context | |
* @return {Emitter} | |
* @api public | |
*/ | |
Emitter.prototype.off = | |
Emitter.prototype.removeListener = | |
Emitter.prototype.removeAllListeners = | |
Emitter.prototype.removeEventListener = function(event, fn, context){ | |
this.callbacks = this.callbacks || {}; | |
// all | |
if (0 == arguments.length) { | |
this.callbacks = {}; | |
return this; | |
} | |
// specific event | |
var callbacks = this.callbacks[event]; | |
if (!callbacks) return this; | |
// remove all handlers | |
if (1 == arguments.length) { | |
delete this.callbacks[event]; | |
return this; | |
} | |
// remove specific handler | |
var cb; | |
for (var i = 0; i < callbacks.length; i++) { | |
cb = callbacks[i]; | |
if (cb.fn === fn && ((!context && !cb.context) || cb.context === context)) { | |
callbacks.splice(i, 1); | |
break; | |
} | |
} | |
return this; | |
}; | |
/** | |
* Emit `event` with the given args. | |
* | |
* @param {Object} event | |
* @return {Emitter} | |
*/ | |
Emitter.prototype.emit = | |
Emitter.prototype.dispatchEvent = function(event){ | |
this.callbacks = this.callbacks || {}; | |
var callbacks = this.callbacks[event.type]; | |
event.target = this; | |
if (callbacks) { | |
callbacks = callbacks.slice(0); | |
for (var i = 0, len = callbacks.length; i < len; ++i) { | |
var callback = callbacks[i]; | |
callback.fn.apply(callback.context || this, arguments); | |
} | |
} | |
return this; | |
}; | |
/** | |
* Return array of callbacks for `event`. | |
* | |
* @param {String} event | |
* @return {Array} | |
* @api public | |
*/ | |
Emitter.prototype.listeners = function(event){ | |
this.callbacks = this.callbacks || {}; | |
return this.callbacks[event] || []; | |
}; | |
/** | |
* Check if this emitter has `event` handlers. | |
* | |
* @param {String} event | |
* @return {Boolean} | |
* @api public | |
*/ | |
Emitter.prototype.hasListeners = function(event){ | |
return !! this.listeners(event).length; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var C = { | |
Bindable: require('./lib/Bindable'), | |
ArrayList: require('./lib/ArrayList'), | |
JSONReviver: require('./lib/JSONReviver'), | |
ClassRegistry: require('./lib/ClassRegistry') | |
}; | |
if (typeof window !== 'undefined') { | |
window.C = C; | |
} | |
module.exports = C; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Bindable = require('./Bindable'); | |
var ArrayList = require('./ArrayList'); | |
var ClassRegistry = require('./ClassRegistry'); | |
function JSONReviver(options) { | |
options = options || {}; | |
var typeKey = options.typeKey || '@class'; | |
function resolveType(value) { | |
var type = value[typeKey]; | |
return type ? ClassRegistry.getClass(type) : undefined; | |
} | |
return function revive(key, value) { | |
if (Array.isArray(value)) { | |
return new ArrayList(value); | |
} | |
if (typeof value === 'object') { | |
var Type = resolveType(value); | |
return Type ? new Type(value) : Bindable.fromObject(value); | |
} | |
return value; | |
}; | |
} | |
module.exports = JSONReviver; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var expect = require('chai').expect; | |
var C = require('..'); | |
describe('JSONReviver', function() { | |
it('should revive', function() { | |
var data = { | |
foo: 'bar', | |
bar: { name: 'baz' }, | |
items: [ | |
'one', | |
{ name: 'two', type: 'two-type' }, | |
[{ name: 'three', number: 3}] | |
] | |
}; | |
var json = JSON.stringify(data); | |
var inflated = JSON.parse(json, C.JSONReviver()); | |
expect(C.Bindable.isBindable(inflated)); | |
expect(C.Bindable.isBindable(inflated.bar)); | |
expect(inflated.bar.name).to.equal('baz'); | |
expect(inflated.items instanceof C.ArrayList); | |
expect(inflated.items.length).to.equal(3); | |
expect(C.Bindable.isBindable(inflated.items.at(2))); | |
expect(inflated.items.at(2) instanceof C.ArrayList); | |
expect(C.Bindable.isBindable(inflated.items.at(2).at(0))); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "catalyst", | |
"version": "0.1.0", | |
"dependencies": { | |
"component-emitter": "^1.1.2", | |
"lodash": "^2.4.1" | |
}, | |
"devDependencies": { | |
"chai": "^1.9.1", | |
"sinon": "^1.9.1" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @jsx React.DOM */ | |
(function() { | |
var Person = React.createClass({ | |
componentWillMount: function() { | |
this.componentWillReceiveProps(this.props); | |
}, | |
componentWillReceiveProps: function(props) { | |
if (props.data) { | |
props.data.on(C.Bindable.CHANGE, this.onChange); | |
} | |
}, | |
render: function() { | |
var data = this.props.data; | |
if (!data) { | |
return ( | |
<div /> | |
); | |
} | |
return ( | |
<div className="preview"> | |
<div className="avatar"> | |
<img id="avatar" src={ data.picture } /> | |
</div> | |
<div className="info"> | |
<h3>Hi, my name is <span className="gen_first_name">{ data.name.first }</span> <span className="gen_last_name">{ data.name.last }</span></h3> | |
<div> | |
<p> | |
Name: <span className="gen_first_name">{ data.name.first }</span> <span className="gen_last_name">{ data.name.last }</span><br /> | |
Gender: <span className="gen_gender">{ data.gender }</span><br /> | |
Email: <span className="gen_email_address">{ data.email }</span><br /> | |
Profile Photo URL: <span className="gen_profile_url"><a href={ data.picture }>{ data.picture }</a></span> | |
</p> | |
<ul> | |
{ data.tags.map(this.renderTag) } | |
</ul> | |
</div> | |
</div> | |
</div> | |
); | |
}, | |
renderTag: function(tag, index) { | |
return ( | |
<li key={ 'tag-' + index }>{ tag }</li> | |
); | |
}, | |
onChange: function(event) { | |
console.log('change', event); | |
this.forceUpdate(); | |
} | |
}); | |
var person = React.renderComponent(<Person />, document.getElementById('person')); | |
console.log('getting json'); | |
$.ajax({ | |
url: 'http://api.randomuser.me/?seed=crazyBird', | |
type: 'get', | |
dataType: 'text' | |
}) | |
.then(function(json) { | |
console.log('got json', json); | |
var data = JSON.parse(json, C.JSONReviver()).results.at(0).user; | |
C.Bindable.defineProperty(data, 'tags', { | |
value: new C.ArrayList(['one', 'two', 'three']) | |
}); | |
window.personData = data; | |
console.log('revived data', data); | |
person.setProps({ data: data }); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment