Last active
December 17, 2015 23:09
-
-
Save twalker/5687541 to your computer and use it in GitHub Desktop.
utility for mixing methods into objects and backbone klasses.
This file contains hidden or 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
/** | |
* Mixer of mixins. | |
* A utility to copy functionality from mixins to objects. | |
*/ | |
define(['underscore'], function(lodash){ | |
// Monkey patch a destination object (i.e. Model.prototype, View.prototype, etc.) | |
// by combining member values that are object literals (e.g. events, defaults), | |
// functions (e.g. initialize), or arrays (e.g.relations). | |
// Heavily inspired by: https://github.com/onsi/cocktail | |
function patch(dest){ | |
var mixins = lodash(arguments).toArray().rest(); | |
var collisions = {}; | |
lodash(mixins).each(function(mixin) { | |
lodash(mixin).forOwn(function(value, key) { | |
if (lodash.isFunction(value)) { | |
// methods | |
if (dest[key]) { | |
collisions[key] = collisions[key] || [dest[key]]; | |
collisions[key].push(value); | |
} | |
dest[key] = value; | |
} else if (lodash.isPlainObject(value)) { | |
// object literals | |
dest[key] = lodash.extend({}, value, dest[key] || {}); | |
} else if(lodash.isArray(value)) { | |
// arrays | |
dest[key] = value.concat(dest[key] || []); | |
} else { | |
// primitives (string, date, regex), last in wins!?! | |
// mixins should be composed of functions, and rarely use primitives. | |
dest[key] = value; | |
} | |
}); | |
}); | |
lodash(collisions).forOwn(function(fnValues, propName) { | |
dest[propName] = wrapFuncs(fnValues); | |
}); | |
} | |
// creates a wrapped function that invokes an array of functions, | |
// in the order given. Returns the last undefined return. | |
function wrapFuncs(funcs){ | |
return function(){ | |
var that = this, | |
args = arguments, | |
returnValue; | |
lodash(funcs).each(function(value) { | |
var returnedValue = lodash.isFunction(value) ? value.apply(that, args) : value; | |
returnValue = (returnedValue === undefined ? returnValue : returnedValue); | |
}); | |
return returnValue; | |
}; | |
} | |
return { | |
/** | |
* Non-destructively copies members to a destination object from source objects. | |
* A simple proxy to lodash.defaults. | |
* | |
* @example | |
* mixer.mixin(MyModel.prototype, modelMixin1, modelMixin2); | |
*/ | |
mixin: lodash.defaults, | |
/** | |
* Destructively copies members to a destination object from source objects. | |
* A simple proxy to lodash.extend. | |
* | |
* @example | |
* mixer.assign(MyModel.prototype, modelMixin1, modelMixin2); | |
*/ | |
assign: lodash.assign, | |
/** | |
* Monkey patches members from mixins into a destination object, | |
* merging colliding members (methods, object literals, and arrays). | |
* Typically used on Constructor (klass) prototypes when a mixin | |
* includes initialize, events, or other conflicting members. | |
* | |
* @example | |
* mixer.patch(MyModel.prototype, { | |
* initialize: function(){return "called after MyModel.initialize"}, | |
* defaults: {labels: []} | |
* }, modelMixin2); | |
*/ | |
patch: patch | |
}; | |
}); |
This file contains hidden or 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
require([ | |
'mocha', | |
'chai', | |
'sinon', | |
'backbone', | |
'mixer' | |
], function(mocha, chai, sinon, Backbone, mixer){ | |
// setup | |
var assert = chai.assert; | |
mocha.setup('bdd'); | |
describe('mixer', function(){ | |
var noop = function noop(){}; | |
describe('.mixin(dest [, source1, source2, …])', function(){ | |
var Mod, modMixin; | |
beforeEach(function(){ | |
Mod = Backbone.Model.extend({ | |
defaults: { first: 'sid'} | |
}); | |
modMixin = { | |
defaults: {last: 'vicious'}, | |
playBass: noop | |
}; | |
mixer.mixin(Mod.prototype, modMixin, {rebel: noop}); | |
}); | |
it('should copy methods/properties to a destination object from sources', function(){ | |
assert.isDefined(Mod.prototype.playBass); | |
assert.isDefined(Mod.prototype.rebel); | |
}); | |
it('should not overwrite existing members', function(){ | |
assert.deepEqual(Mod.prototype.defaults, {first:'sid'}); | |
var bestPunk = {}; | |
mixer.mixin(bestPunk, {band: 'sex pistols'}, {band: 'the clash'}); | |
assert.equal(bestPunk.band, 'sex pistols'); | |
}); | |
}); | |
describe('.assign(dest [, source1, source2, …])', function(){ | |
var Mod, modMixin; | |
beforeEach(function(){ | |
Mod = Backbone.Model.extend({ | |
defaults: {first: 'sid'} | |
}); | |
modMixin = { | |
defaults: {last: 'vicious'}, | |
playBass: noop | |
}; | |
mixer.assign(Mod.prototype, modMixin, {rebel: noop}); | |
}); | |
it('should copy methods/properties to a destination object from sources', function(){ | |
assert.isDefined(Mod.prototype.playBass); | |
assert.isDefined(Mod.prototype.rebel); | |
}); | |
it('should overwrite existing members', function(){ | |
assert.deepEqual(Mod.prototype.defaults, {last:'vicious'}); | |
var bestPunk = {}; | |
mixer.assign(bestPunk, {band: 'sex pistols'}, {band: 'the clash'}); | |
assert.equal(bestPunk.band, 'the clash'); | |
}); | |
}); | |
describe('.patch(dest [, source1, source2, …])', function(){ | |
var Mod, modMixin; | |
var initSpy, mixinInitSpy; | |
beforeEach(function(){ | |
initSpy = sinon.spy(); | |
mixinInitSpy = sinon.spy(); | |
Mod = Backbone.Model.extend({ | |
defaults: {first: 'sid'}, | |
initialize: initSpy, | |
genre: ['punk'] | |
}); | |
modMixin = { | |
defaults: {last: 'vicious'}, | |
playBass: noop, | |
initialize: mixinInitSpy, | |
genre: ['rock'] | |
}; | |
mixer.patch(Mod.prototype, modMixin, {rebel: noop}); | |
}); | |
it('should copy methods/functions to a destination object from sources', function(){ | |
assert.isDefined(Mod.prototype.playBass); | |
assert.isDefined(Mod.prototype.rebel); | |
}); | |
it('should merge colliding/existing members (functions, plain objects, and arrays)', function(){ | |
assert.deepEqual(Mod.prototype.defaults, {first:'sid', last: 'vicious'}); | |
assert.deepEqual(Mod.prototype.genre.sort(), ['punk', 'rock'].sort()); | |
var options = {rock: 'hard'}; | |
new Mod({}, options); | |
assert.isTrue(initSpy.called); | |
assert.isTrue(mixinInitSpy.called); | |
assert.isTrue(initSpy.calledWith({}, options)); | |
assert.isTrue(mixinInitSpy.calledWith({}, options)); | |
}); | |
it('should merge N collisions in their source order', function(){ | |
var thirdSpy = sinon.spy(); | |
mixer.patch(Mod.prototype, {initialize: thirdSpy}); | |
var m = new Mod({}); | |
assert.isTrue(initSpy.called); | |
assert.isTrue(initSpy.calledBefore(mixinInitSpy)); | |
assert(initSpy.calledOnce); | |
assert(initSpy.calledOn(m)); | |
assert(mixinInitSpy.called); | |
assert(mixinInitSpy.calledBefore(thirdSpy)); | |
assert(thirdSpy.called); | |
assert(thirdSpy.calledAfter(mixinInitSpy)); | |
assert(thirdSpy.calledOn(m)); | |
}); | |
it('should not break the prototype chain', function(){ | |
var parentSpy = sinon.spy(), | |
childSpy = sinon.spy(), | |
mixSpy = sinon.spy(); | |
var ParentView = Backbone.View.extend({ | |
initialize: parentSpy, | |
events: {'click':'onParent'} | |
}); | |
var ChildView = ParentView.extend({ | |
initialize: childSpy, | |
events: {'click':'onChild'} | |
}); | |
var viewMix = { | |
initialize: mixSpy, | |
events: {'submit': 'onMix'} | |
}; | |
mixer.patch(ChildView.prototype, viewMix); | |
var view = new ChildView(); | |
assert.isFalse(parentSpy.called); | |
assert.isTrue(childSpy.called); | |
assert.isTrue(mixSpy.called); | |
assert.deepEqual(ParentView.prototype.events, {'click':'onParent'}); | |
assert.deepEqual(ChildView.prototype.events, {'click':'onChild', 'submit': 'onMix'}); | |
delete ChildView.prototype.events; | |
delete ChildView.prototype.initialize; | |
// child view should now use prototype of parent | |
assert.deepEqual(ChildView.prototype.events, ParentView.prototype.events); | |
assert.equal(ChildView.prototype.initialize, ParentView.prototype.initialize) | |
}); | |
}); | |
}); | |
// Start runner | |
mocha.run(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment