diff --git a/src/bindAll.spec.ts b/src/bindAll.spec.ts index 0e59495..5887c19 100644 --- a/src/bindAll.spec.ts +++ b/src/bindAll.spec.ts @@ -100,4 +100,13 @@ describe('bindAll', () => { expect(myClass.hasOwnProperty('prop')).to.be.false; expect(myClass.hasOwnProperty('prop2')).to.be.false; }); + + it('should copy over any static properties on the constructor', () => { + @BindAll() + class MyClass { + static $inject = []; + } + + expect(MyClass.$inject).to.be.an('array'); + }); }); diff --git a/src/bindAll.ts b/src/bindAll.ts index 6d3cdf9..05a2afc 100644 --- a/src/bindAll.ts +++ b/src/bindAll.ts @@ -1,6 +1,6 @@ import isFunction = require('lodash/isFunction'); -import { copyMetadata } from './utils'; +import { copyMetadata, wrapConstructor } from './utils'; import { InstanceChainMap } from './factory'; /** @@ -28,15 +28,11 @@ import { InstanceChainMap } from './factory'; */ export function BindAll(methods: string[] = []): ClassDecorator { return (target: Function): Function => { - function BindAllWrapper(...args: any[]): any { + return wrapConstructor(target, function(Ctor: Function, ...args: any[]) { bindAllMethods(target, this, methods); - target.apply(this, args); - } - - BindAllWrapper.prototype = target.prototype; - - return BindAllWrapper; + Ctor.apply(this, args); + }); }; } diff --git a/src/utils/assignAll.ts b/src/utils/assignAll.ts new file mode 100644 index 0000000..6fed20c --- /dev/null +++ b/src/utils/assignAll.ts @@ -0,0 +1,47 @@ +import without = require('lodash/without'); +import attempt = require('lodash/attempt'); +import isObject = require('lodash/isObject'); + +/** + * Assigns all properties from an object to another object including non enumerable + * properties. + * @export + * @template T + * @template U + * @param {T} to + * @param {U} from + * @param {string[]} [excludes=[]] + * @returns {T} + */ +export function assignAll(to: T, from: U, excludes: string[] = []): T { + const properties = without(Object.getOwnPropertyNames(from), ...excludes); + + for (const prop of properties) { + attempt(assignProperty, to, from, prop); + } + + return to; +} + +/** + * Assigns a property from one object to another while retaining descriptor properties. + * @export + * @template T + * @template U + * @param {T} to + * @param {U} from + * @param {string} prop + */ +export function assignProperty(to: T, from: U, prop: string): void { + const descriptor = Object.getOwnPropertyDescriptor(to, prop); + + if (!descriptor || descriptor.configurable) { + const srcDescriptor = Object.getOwnPropertyDescriptor(from, prop); + + if (isObject(srcDescriptor)) { + Object.defineProperty(to, prop, srcDescriptor); + } else { + (to as any)[prop] = (from as any)[prop]; + } + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 50145e3..51441bd 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,4 +3,6 @@ export * from './resolveFunction'; export * from './CompositeKeyWeakMap'; export * from './returnAtIndex'; export * from './copyMetaData'; -export * from './bind'; \ No newline at end of file +export * from './bind'; +export * from './wrapConstructor'; +export * from './assignAll'; diff --git a/src/utils/wrapConstructor.ts b/src/utils/wrapConstructor.ts new file mode 100644 index 0000000..b967ef4 --- /dev/null +++ b/src/utils/wrapConstructor.ts @@ -0,0 +1,27 @@ +import { assignAll } from './assignAll'; + +const PROPERTY_EXCLUDES = [ + 'length', + 'name', + 'arguments', + 'called', + 'prototype' +]; + +/** + * Wraps a constructor in a wrapper function and copies all static properties + * and methods to the new constructor. + * @export + * @param {Function} Ctor + * @param {(Ctor: Function, ...args: any[]) => any} wrapper + * @returns {Function} + */ +export function wrapConstructor(Ctor: Function, wrapper: (Ctor: Function, ...args: any[]) => any): Function { + function ConstructorWrapper(...args: any[]) { + return wrapper.call(this, Ctor, ...args); + } + + ConstructorWrapper.prototype = Ctor.prototype; + + return assignAll(ConstructorWrapper, Ctor, PROPERTY_EXCLUDES); +}