mixin.js

/**
 * Mixin decorator.
 * @function mixin
 *
 * @license {@link https://opensource.org/licenses/MIT|MIT}
 *
 * @author Patrick Heng <hengpatrick.pro@gmail.com>
 * @author Fabien Motte <contact@fabienmotte.com>
 *
 * @param {...Object} mixins Mixin(s).
 *
 * @throws {SyntaxError} Mixin decorator requires at least one mixin.
 *
 * @returns {function} Wrapped target class.
 */
export default function mixin (...mixins) {
  if (typeof mixins[0] === 'function') {
    throw new SyntaxError(`@mixin decorator '${mixins[0].name}' requires at least one mixin`)
  }

  // Return wrapped target class
  return function ({ prototype }) {
    const mergedFunctions = {}

    // Parse mixins
    for (const mixin of mixins) {
      for (const key in mixin) {
        const prop = mixin[key]

        if (typeof prop === 'function') {
          mergedFunctions[key] = mergedFunctions[key] || []
          mergedFunctions[key].push(prop)
        } else {
          prototype[key] = prop
        }
      }
    }

    // Rewrite prototype function
    for (const key in mergedFunctions) {
      const functions = mergedFunctions[key]
      const defaultFn = prototype[key]

      prototype[key] = function (...args) {
        let result

        for (const fn of functions) {
          result = fn.apply(this, args)
        }

        if (typeof defaultFn === 'function') {
          result = defaultFn.apply(this, args)
        }

        return result
      }
    }
  }
}