Permalink
Browse files

Mixins now play nicely with class hierarchies.

  • Loading branch information...
1 parent c426428 commit a13d61d33d6423672ab2ea9f73e85abe9fae8e25 @onsi committed Jul 25, 2012
Showing with 122 additions and 57 deletions.
  1. +2 −2 Cocktail.js
  2. +8 −2 README.md
  3. +112 −53 spec/spec/CocktailSpec.js
View
@@ -11,15 +11,15 @@
var klass = originalExtend.call(this, protoProps, classProps);
var mixins = klass.prototype.mixins;
- if (mixins && mixins.length > 0) {
+ if (mixins && klass.prototype.hasOwnProperty('mixins')) {
var collisions = {};
_(mixins).each(function(mixin) {
_(mixin).each(function(value, key) {
if (key == 'events') {
klass.prototype.events = _.extend({}, klass.prototype.events || {}, value);
} else if (_.isFunction(value)) {
- if (_.has(klass.prototype, key)) {
+ if (klass.prototype[key]) {
collisions[key] = collisions[key] || [klass.prototype[key]];
collisions[key].push(value);
}
View
@@ -12,7 +12,7 @@ Here's an example mixin that implements selectability on a view based on a model
MyMixins.SelectMixin = {
initialize: function() {
- this.model.on('change:select', this.refreshSelect, this);
+ this.model.on('change:selected', this.refreshSelect, this);
},
events: {
@@ -44,7 +44,7 @@ Once you have your mixins defin;;ed including them in your Backbone object defin
var MyView = Backbone.View.extend({
- mixins: [MyMixins.SelectMixin, MyMixins.SometherMixin],
+ mixins: [MyMixins.SelectMixin, MyMixins.SomeOtherMixin],
events: {
'click .myChild': 'myCustomHandler'
@@ -91,6 +91,12 @@ The events hash is special-cased by Cocktail. Mixins can define new events hash
Note that key-collisions are still possible. If two mixins add a `click` handler to the events hash (`{'click': ... }`) then the last mixin in the mixins list's event handler will win.
+## And what about subclasses?
+
+Subclass hierarchies with mixins should work just fine. If a super class mixes in a mixin, then all subclasses will inherit that mixin. If those subclasses mixin additional mixins, those mixins will be folded in to the subclasses and collisions will be handled correctly, even collisions with methods further up the class hierarchy.
+
+However, if a subclass redefines a method that is provided by a mixin of the super class, the mixin's implementation will *not* be called. This shouldn't be surprising: the subclass's method is further up in the prototype chain and is the method that gets evaluated. In these circumstance you *must* remember to call `SubClass.__super__.theMethod.apply(this)` to ensure that the mixin's method gets called.
+
## Testing Mixins
The [example](https://github.com/onsi/cocktail/tree/master/example) directory includes an example mixin and its usage, and the accompanying [Jasmine](http://www.github.com/pivotal/jasmine) test. It also includes a [readme](https://github.com/onsi/cocktail/tree/master/example) that walks through the testing pattern employed for testing mixins with Jasmine.
View
@@ -75,73 +75,132 @@ describe('Cocktail', function() {
initialize: function() {
this.$el.append('<div class="view"></div>');
- },
-
- clickView: function() {
- calls.push('clickView');
- },
+ },
- render: function() {
- calls.push('renderView');
- return this;
- },
+ clickView: function() {
+ calls.push('clickView');
+ },
- beforeTearDown: function() {
- calls.push('beforeTearDownView');
- },
+ render: function() {
+ calls.push('renderView');
+ return this;
+ },
- awesomeSauce: function() {
- calls.push('awesomeView')
- }
- });
+ beforeTearDown: function() {
+ calls.push('beforeTearDownView');
+ },
+
+ awesomeSauce: function() {
+ calls.push('awesomeView')
+ }
+ });
});
- describe('mixing in mixins', function() {
- it('should mixin all the mixin methods for all the mixins', function() {
- view = new ViewClass();
- view.theFunc();
- expect(calls).toContain('theFunc');
- view.sublime();
- expect(calls).toContain('sublime');
- });
+describe('mixing in mixins', function() {
+ it('should mixin all the mixin methods for all the mixins', function() {
+ view = new ViewClass();
+ view.theFunc();
+ expect(calls).toContain('theFunc');
+ view.sublime();
+ expect(calls).toContain('sublime');
+ });
- describe('the events hash', function() {
- it("should preserve the original view's events and mixin the events hashes for the mixins", function() {
- view = new ViewClass();
+ describe('the events hash', function() {
+ it("should preserve the original view's events and mixin the events hashes for the mixins", function() {
+ view = new ViewClass();
- $('#content').append(view.$el);
+ $('#content').append(view.$el);
- $('.A').click();
- $('.B').click();
- $('.view').click();
+ $('.A').click();
+ $('.B').click();
+ $('.view').click();
- expect(calls).toEqual(['clickA', 'clickB', 'clickView']);
- });
+ expect(calls).toEqual(['clickA', 'clickB', 'clickView']);
});
});
+});
+
+describe('handling method collisions', function() {
+ it('should call all the methods involved in the collision in the correct order', function() {
+ view = new ViewClass();
+ view.render();
+ view.awesomeSauce();
+ view.fooBar();
+ $('#content').append(view.$el);
+ expect($('.A')[0]).toBeTruthy();
+ expect($('.B')[0]).toBeTruthy();
+ expect($('.view')[0]).toBeTruthy();
+ view.beforeTearDown();
+
+ expect(calls).toEqual(['renderView', 'renderA', 'awesomeView', 'awesomeA', 'fooBarA', 'fooBarB','beforeTearDownView', 'beforeTearDownB']);
+ });
+
+ it('should return the last return value in the collision chain', function() {
+ view = new ViewClass();
+ expect(view.render()).toEqual(view);
+ expect(view.theFunc()).toEqual('func!');
+ expect(view.sublime()).toEqual('sublemon');
+ expect(view.fooBar()).toEqual(false);
+ expect(view.awesomeSauce()).toEqual('the sauce');
+ });
+});
- describe('handling method collisions', function() {
- it('should call all the methods involved in the collision in the correct order', function() {
- view = new ViewClass();
- view.render();
- view.awesomeSauce();
- view.fooBar();
- $('#content').append(view.$el);
- expect($('.A')[0]).toBeTruthy();
- expect($('.B')[0]).toBeTruthy();
- expect($('.view')[0]).toBeTruthy();
- view.beforeTearDown();
-
- expect(calls).toEqual(['renderView', 'renderA', 'awesomeView', 'awesomeA', 'fooBarA', 'fooBarB','beforeTearDownView', 'beforeTearDownB']);
+describe('when mixins are applied in the context of super/subclasses', function() {
+ var BaseClass, SubClass, SubClassWithMixin;
+ beforeEach(function() {
+ BaseClass = Backbone.View.extend({
+ mixins: [A],
+ fooBar: function() {
+ calls.push('BaseClassFoo');
+ }
});
- it('should return the last return value in the collision chain', function() {
- view = new ViewClass();
- expect(view.render()).toEqual(view);
- expect(view.theFunc()).toEqual('func!');
- expect(view.sublime()).toEqual('sublemon');
- expect(view.fooBar()).toEqual(false);
- expect(view.awesomeSauce()).toEqual('the sauce');
+ SubClass = BaseClass.extend({
+ fooBar: function() {
+ SubClass.__super__.fooBar.apply(this);
+ calls.push('SubClassFoo')
+ }
});
+
+ SubClassWithMixin = BaseClass.extend({
+ mixins: [B],
+
+ fooBar: function() {
+ SubClassWithMixin.__super__.fooBar.apply(this);
+ calls.push('SubClassWithMixinFoo')
+ }
+ });
+ });
+
+ it("should behave correctly for the base class", function() {
+ baseInstance = new BaseClass();
+
+ $('#content').append(baseInstance.$el);
+ $('.A').click();
+ baseInstance.fooBar();
+
+ expect(calls).toEqual(['clickA', 'BaseClassFoo', 'fooBarA'])
+ });
+
+ it("should behave correctly for the sub class that has no mixins", function() {
+ subInstance = new SubClass();
+
+ $('#content').html(subInstance.$el);
+ $('.A').click();
+ subInstance.fooBar();
+
+ expect(calls).toEqual(['clickA', 'BaseClassFoo', 'fooBarA', 'SubClassFoo']);
+ });
+
+ it("should behave correctly for the sub class that has mixins", function() {
+ subInstance = new SubClassWithMixin();
+
+ $('#content').html(subInstance.$el);
+ $('.A').click();
+ $('.B').click();
+ subInstance.fooBar();
+
+ expect(calls).toEqual(['clickA', 'clickB', 'BaseClassFoo', 'fooBarA', 'SubClassWithMixinFoo', 'fooBarB']);
});
+});
});

0 comments on commit a13d61d

Please sign in to comment.