API Docs for:
Show:

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 }