File: development/js/utils/classes.js
/**
* Provides utility functions to ease development of class based hierarchies
*
* @module Utils
* @requires Utils.Types
* @class Utils.Classes
* @static
*/
import type from './type';
/**
* Creates a mixin function from a class descriptor. There are some rules you
* should follow when working with mixins. A mixin should not define its own
* constructor function but only methods which extend any given
* class. A mixin should be self-contained and it should not make assumptions
* about the shape of any class it can be mixed into. This ensures mixins
* will not be bound to a certain class and can be shared freely. Please note
* that mixins cannot override methods which already exist, as this
* would lead to massive complexity when using mixins together with normal
* classes because it is no longer clear where a certain implementation
* comes from. One way to work around this and increase readability is to
* prefix each mixin method with the name of the mixin itself. Don't
* define properties because those won't be mixed into - a mixin should only
* contain methods and define all properties it needs during runtime from
* methods. See also the examples below
*
* @method Mixin
* @for Utils.Classes
* @param {object} proto The class descriptor
* @return {function} The mixin function
*
* @example
* import {Mixin} from './utils/classes';
*
* // this example shows how to use mixins with a single class
*
* const Bar = Mixin({
* barInit() {
* if (!this.__barValue) {
* this.__barValue = 'BAR';
* }
* return this;
* },
* barMethod() { // returns the barValue or creates and returns it
* return this.__barValue || this.barInit().__barValue;
* }
* });
*
* const Foo = Bar.mixedInto(class {
* foo() { return `FOO ${this.barMethod()}`; }
* });
*
*
* const myFoo = new Foo();
* myFoo.foo(); // -> 'FOO BAR'
*
* myFoo instanceof Foo; // -> true
* myFoo instanceof Bar; // -> true
*
*
*
* @example
* import {Mixin} from './utils/classes';
*
* // this example shows how to use mixins with a base class and a class which
* // inherits from the base class and the mixin
*
* const Bar = Mixin({
* barMethod() { return 'BAR'; }
* });
*
* class Foo {
* foo() { return 'FOO'; }
* }
*
* class Baz extends Bar.mixedInto(Foo) {
* baz() { return `${this.foo()} ${this.barMethod()} BAZ`; }
* }
*
* // equivalent:
* class Baz extends Bar(Foo) {
* baz() { ... }
* }
*
*
*
* const myBaz = new Baz();
* myBaz.baz(); // -> 'FOO BAR BAZ'
*
* myBaz instanceof Foo; // -> true
* myBaz instanceof Bar; // -> true
* myBaz instanceof Baz; // -> true
*/
export const Mixin = (proto) => {
const keys = Object.keys(proto);
const typeTag = Symbol('isA');
function ClassMixin (BaseClass) {
let MixedClass = class extends BaseClass {};
if (!BaseClass.prototype[typeTag]) {
for (let prop of keys) {
if (!MixedClass.prototype[prop]) {
Object.defineProperty(MixedClass.prototype, prop, {
value: proto[prop],
writable: true
});
}
}
Object.defineProperty(MixedClass.prototype, typeTag, {value: true});
}
return MixedClass;
}
Object.defineProperty(ClassMixin, Symbol.hasInstance, {
value: (x) => !!x[typeTag]
});
Object.defineProperty(ClassMixin, 'isMixin', {
value: true,
writable: false
});
Object.defineProperty(ClassMixin, 'mixedInto', {
value: (SuperClass) => ClassMixin(SuperClass),
writable: false
});
return ClassMixin;
}
/**
* Concattenates many mixins into a single mixin. Allows to pass either already
* defined mixins or just plain objects which will be converted to mixins
* on the fly. Returns a functions which accepts a base class and returns a
* new class which inherits from the base class and all mixins
*
* @method MixinTrait
* @for Utils.Classes
* @param {function|object} ...mixins All mixins to build a trait from
* @return {function} The mixin trait
*
* @example
* import {Mixin, MixinTrait} from './utils/classes';
*
* const Bar = Mixin({
* barMethod() { return 'BAR'; }
* });
*
* const Foo = Mixin({
* fooMethod() { return 'FOO'; }
* });
*
* const FooBar = MixinTrait(Foo, Bar);
*
*
*
* const Baz = FooBar.mixedInto(class {
* baz() { return `${this.fooMethod()} ${this.barMethod()} BAZ`; }
* });
*
* // equivalent:
* const Baz = FooBar(class {
* baz() { ... }
* });
*
* // without "MixinTrait" this would be:
* const Baz = Foo.mixedInto(Bar.mixedInto(class {
* baz() { ... }
* }));
*
*
*
* const myBaz = new Baz();
* myBaz.baz(); // -> 'FOO BAR BAZ'
*
* myBaz instanceof Foo; // -> true
* myBaz instanceof Bar; // -> true
* myBaz instanceof FooBar; // -> true
* myBaz instanceof Baz; // -> true
*/
export const MixinTrait = (...mixins) => {
const typeTag = Symbol('isA');
function TraitMixin (SuperClass) {
let TraitClass = mixins.reduce((a, m) => {
if (type.isFunction(m) && m.isMixin) {
return m(a);
}
if (type.isObject(m)) {
return Mixin(m)(a);
}
return a;
}, class extends SuperClass {});
Object.defineProperty(TraitClass.prototype, typeTag, {value: true});
return TraitClass;
}
Object.defineProperty(TraitMixin, Symbol.hasInstance, {
value: (x) => !!x[typeTag]
});
Object.defineProperty(TraitMixin, 'isMixin', {
value: true,
writable: false
});
Object.defineProperty(TraitMixin, 'mixedInto', {
value: (SuperClass) => TraitMixin(SuperClass),
writable: false
});
return TraitMixin;
}
export default { Mixin, MixinTrait }