Skip to content

Instantly share code, notes, and snippets.

@eschwartz
Last active January 1, 2016 19:09
Show Gist options
  • Save eschwartz/8188988 to your computer and use it in GitHub Desktop.
Save eschwartz/8188988 to your computer and use it in GitHub Desktop.
WireJS Plugin: ListenTo Facet
define([
'vendor/underscore',
'when'
], function(_, when) {
/**
* Helper class for
* the listenTo facet.
*
* @param proxy
* @param wire
*
* @constructor
*/
var ListenToFacet = function(proxy, wire) {
this.listener_ = proxy.target;
this.spec_ = proxy.options;
this.wire_ = wire;
};
/**
* Bind event listeners, as
* defined in the facet spec.
*
* @returns {when.promise} A promise to resolve all listeners.
*/
ListenToFacet.prototype.connect = function() {
var connectionPromises = [];
_.each(this.spec_, function(eventSpec, targetRef) {
connectionPromises.push(this.connectTarget(eventSpec, targetRef))
}, this);
return when.all(connectionPromises);
};
/**
* Bind listeners to a target object.
*
* @param {Object} eventSpec
* As: {
* eventName: 'opt_transform | handler',
* ...
* }
*
* @param {string} targetRef Reference to target object
* @returns {when.promise} Promise to bind target events.
*/
ListenToFacet.prototype.connectTarget = function(eventSpec, targetRef) {
var passThrough = function(args) { return args; }
var whenTargetResolved = this.wire_.resolveRef(targetRef);
var connectionPromises = [];
_.each(eventSpec, function(handlerSpec, topic) {
var whenTransformerResolved;
var whenConnected;
var handler;
var parts = handlerSpec.split('|');
// HandlerSpec: 'transformer | handler'
// --> Resolve transformer
if (parts.length === 2) {
whenTransformerResolved = this.resolveTransformer(parts[0].trim());
handler = this.listener_[parts[1].trim()];
}
// HandlerSpec: 'handler'
// --> Use a pass-through transformer
else {
whenTransformerResolved = when(passThrough);
handler = this.listener_[handlerSpec];
}
// Transformer and target are resolved
whenConnected = when.join(whenTargetResolved, whenTransformerResolved).
then(_.bind(function(refs) {
var target = refs[0];
var transformer = refs[1];
this.listenTo(target, topic, handler, transformer);
}, this));
connectionPromises.push(whenConnected);
}, this);
return when.all(connectionPromises);
};
/**
* @param {string} transformerSpec As '[opt_ns].transformer'
* @return {when} Promise to resolve with transformer function.
*/
ListenToFacet.prototype.resolveTransformer = function(transformerSpec) {
var specParts = transformerSpec.split('.');
var nsRef, transformerRef;
return when.promise(_.bind(function(resolve, reject) {
// Spec: 'namespace.transformer'
// --> Resolve namespace reference
if (specParts.length === 2) {
nsRef = specParts[0].trim();
transformerRef = specParts[1].trim();
this.wire_.resolveRef(nsRef).then(function(ns) {
resolve(ns[transformerRef]);
});
}
// Spec: 'transformer'
// --> Resolve transformer reference
else {
transformerRef = transformerSpec;
this.wire_.resolveRef(transformerRef).then(resolve, reject);
}
}, this));
};
/**
* Listen to a topic emitted by a target,
* using a handler and a transformer.
*
* @param {Backbone.Events} target
* @param {string} topic
* @param {Function} handler
* @param {Function} transformer
*/
ListenToFacet.prototype.listenTo = function(target, topic, handler, transformer) {
this.listener_.listenTo(target, topic, _.bind(function(var_params) {
var args = _.argsToArray(arguments);
var tranformedParams = transformer.apply(transformer, args);
handler.call(this.listener_, tranformedParams);
}, this))
};
/**
* String#trim method shim
*
* From:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
*/
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
};
}
return function() {
return {
facets: {
/**
* ListenTo Facet.
*
* Binds a listener object to events thrown
* by a target object.
*
* Both objects must mixin Backbone.Events: this facet
* uses the Backbone.Events#listenTo / #stopListeningTo
* methods to bind events.
*
* Parameters attached to events can be optionally
* transformed by transformer methods. Transformer methods
* receive an array of event parameters, and return a
* transformed array of arguments to pass on to
* the event handler.
*
* Example:
* // Transformer
* define('shout', function() {
* return function(talker, words) {
* return words.toUpperCase();
* }
* });
*
* wire({
* shout: { module: 'shout' },
* talker: { create: 'talker' },
* listener: {
* create: 'listener',
* listenTo: {
* talker: {
* whisper: 'shout | listen'
* }
* }
* }
* });
*
* talker.trigger('whisper', talker, 'hello there');
* // listener called with: 'HELLO THERE'
*
*
* Transformers can also be referenced from
* within an object.
*
* Example:
* define('transformers', {
* shout: function(args) { ... }
* });
*
* wire({
* // ...
* transformers: { module: 'transformers' },
* listener: {
* listenTo: {
* talker: {
* whisper: 'transformers.shout | listen'
* }
* }
* }
* });
*
*
* Transform can be specified as:
* 'methodName' // A method defined in the spec.
* 'otherObj.methodName' // A method of otherObj (where otherObj is defined in the spec)
*/
listenTo: {
ready: function(resolver, proxy, wire) {
var facet = new ListenToFacet(proxy, wire);
facet.connect().then(function() {
resolver.resolve();
}, resolver.reject);
},
destroy: function(resolver, proxy, wire) {
// This is crude, but it works.
proxy.target.stopListening();
resolver.resolve();
}
}
}
};
};
});
define([
'vendor/underscore',
'testUtils',
'wire',
'vendor/backbone'
], function(_, testUtil, wire, Backbone) {
var plugins = ['application/plugin/events'];
var Listener = function() {
this.listen = jasmine.createSpy('Listener#listen');
this.listenClosely = jasmine.createSpy('Listener#listenHard');
};
_.extend(Listener.prototype, Backbone.Events);
define('listener', function() {
return Listener;
});
var Talker = function() {
};
_.extend(Talker.prototype, Backbone.Events);
define('talker', function() {
return Talker;
});
var throwUncatchable = function(e) {
_.defer(function() {
throw e;
})
};
var loudspeaker = jasmine.createSpy('loudspeaker').
andCallFake(function(talker, words) {
return words.toUpperCase();
});
define('loudspeaker', function() {
return loudspeaker;
});
define('transformers', {
loudspeaker: loudspeaker,
muffler: function(talk, words) {
return words.toLowerCase()
}
});
describe('The wire listenTo facet', function() {
describe('listenTo facet', function () {
it('should listen to multiple events from multiple emitters', function () {
wire({
talkerA: {
create: 'talker'
},
talkerB: {
create: 'talker'
},
listener: {
create: 'listener',
listenTo: {
talkerA: {
talk: 'listen',
whisper: 'listenClosely'
},
talkerB: {
talk: 'listen',
whisper: 'listenClosely'
}
}
},
plugins: plugins
}).then(function (ctx) {
ctx.talkerA.trigger('talk', 'hello you');
expect(ctx.listener.listen).toHaveBeenCalledWith('hello you');
ctx.talkerA.trigger('whisper', 'hey guy');
expect(ctx.listener.listenClosely).toHaveBeenCalledWith('hey guy');
testUtil.setFlag();
}).otherwise(throwUncatchable);
waitsFor(testUtil.checkFlag, 1000, 'Wire to complete');
});
it('should transform event data', function () {
wire({
talker: {
create: 'talker'
},
loudspeaker: { module: 'loudspeaker' },
listener: {
create: 'listener',
listenTo: {
talker: {
whisper: 'loudspeaker | listen'
}
}
},
plugins: plugins
}).then(function (ctx) {
ctx.talker.trigger('whisper', ctx.talker, 'hey guy');
expect(ctx.listener.listen).toHaveBeenCalledWith('HEY GUY');
testUtil.setFlag();
}).otherwise(throwUncatchable);
waitsFor(testUtil.checkFlag, 1000, 'Wire to complete');
});
it('should find a transformer within a namespace', function () {
wire({
talker: {
create: 'talker'
},
transformers: { module: 'transformers' },
listener: {
create: 'listener',
listenTo: {
talker: {
whisper: 'transformers.loudspeaker | listen',
shout: 'transformers.muffler | listen'
}
}
},
plugins: plugins
}).then(function (ctx) {
ctx.talker.trigger('whisper', ctx.talker, 'hey guy');
expect(ctx.listener.listen).toHaveBeenCalledWith('HEY GUY');
ctx.talker.trigger('shout', ctx.talker, 'YO DUDE');
expect(ctx.listener.listen).toHaveBeenCalledWith('yo dude');
testUtil.setFlag();
}).otherwise(throwUncatchable);
waitsFor(testUtil.checkFlag, 1000, 'Wire to complete');
});
it('should clear all listeners when the context is destroyed', function() {
spyOn(Listener.prototype, 'stopListening');
wire({
talkerA: {
create: 'talker'
},
talkerB: {
create: 'talker'
},
listener: {
create: 'listener',
listenTo: {
talkerA: {
talk: 'listen',
whisper: 'listenClosely'
},
talkerB: {
talk: 'listen',
whisper: 'listenClosely'
}
}
},
plugins: plugins
}).then(function(ctx) {
return ctx.destroy();
}).
then(function() {
expect(Listener.prototype.stopListening).toHaveBeenCalled();
testUtil.setFlag();
}).
otherwise(throwUncatchable);
waitsFor(testUtil.checkFlag, 1000, 'Wire to complete');
});
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment