diff --git a/src/globals.d.ts b/src/globals.d.ts index 4402188..f346c1b 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,22 +1,22 @@ declare var global: any; interface BrowserNodeGlobal { - Object: typeof Object; - Array: typeof Array; - //Map: typeof Map; - //Set: typeof Set; - Date: typeof Date; - RegExp: typeof RegExp; - JSON: typeof JSON; - Math: typeof Math; + Object: typeof Object, + Array: typeof Array, + //Map: typeof Map, + //Set: typeof Set, + Date: typeof Date, + RegExp: typeof RegExp, + JSON: typeof JSON, + Math: typeof Math, angular: angular.IAngularStatic, - //assert(condition: any): void; - //Reflect: any; - //zone: Zone; - //getAngularTestability: Function; - //getAllAngularTestabilities: Function; - setTimeout: Function; - clearTimeout: Function; - setInterval: Function; - clearInterval: Function; + //assert(condition: any): void, + Reflect: any, + //zone: Zone, + //getAngularTestability: Function, + //getAllAngularTestabilities: Function, + setTimeout: Function, + clearTimeout: Function, + setInterval: Function, + clearInterval: Function } diff --git a/src/util/decorators.ts b/src/util/decorators.ts new file mode 100644 index 0000000..a85511a --- /dev/null +++ b/src/util/decorators.ts @@ -0,0 +1,239 @@ +import {global, Type, isFunction, isString, isPresent} from '../facade/lang'; + + +/** + * @internal + * @private + * @type {string} + */ +export const CLASS_META_KEY = '__mAnnotations'; +/** + * @internal + * @private + * @type {string} + */ +export const PARAM_META_KEY = '__mParameters'; +/** + * @internal + * @private + * @type {string} + */ +export const PROP_META_KEY = '__mPropMetadata'; + +/** + * An interface implemented by all Angular type decorators, + * which allows them to be used as ES7 decorators + * + * ES7 syntax: + * + * ``` + * @Component({...}) + * class MyClass {...} + * ``` + */ +export interface TypeDecorator { + /** + * Invoke as ES7 decorator. + */ + ( type: T ): T; + + // Make TypeDecorator assignable to built-in ParameterDecorator type. + // ParameterDecorator is declared in lib.d.ts as a `declare type` + // so we cannot declare this interface as a subtype. + // see https://github.com/angular/angular/issues/3379#issuecomment-126169417 + ( target: Object, propertyKey?: string | symbol, parameterIndex?: number ): void; + +} + +function extract( metaKey: string ) { + + return function ( cls: any ): any { + + if ( isFunction( cls ) && cls.hasOwnProperty( metaKey ) ) { + // it is a decorator, extract annotation + return cls[ metaKey ]; + } + + } + +} +export function extractAnnotation( cls: any ): any { + + return extract( CLASS_META_KEY )(cls); + +} + +export function extractParameter( cls: any ): any { + + return extract( PARAM_META_KEY )(cls); + +} +export function extractProperty( cls: any ): any { + + return extract( PROP_META_KEY )(cls); + +} + +// This will be needed when we will used Reflect APIs +/*const Reflect = global.Reflect; + if (!(Reflect && Reflect.getMetadata)) { + throw 'reflect-metadata shim is required when using class decorators'; + }*/ + + +export function makeDecorator( + AnnotationCls: any, + chainFn: ( fn: Function ) => void = null +): ( ...args: any[] ) => ( cls: any ) => any { + + function DecoratorFactory( objOrType ): ( cls: any ) => any { + + var annotationInstance = new AnnotationCls( objOrType ); + + if ( this instanceof AnnotationCls ) { + + return annotationInstance; + + } else { + + //var chainAnnotation = isFunction( this ) && this.annotations instanceof Array + // ? this.annotations + // : []; + //chainAnnotation.push(annotationInstance); + + function TypeDecorator( cls ): TypeDecorator { + + //var annotations = Reflect.getOwnMetadata('annotations', cls); + var annotations = cls[ CLASS_META_KEY ]; + + annotations = annotations || []; + annotations.push( annotationInstance ); + + //Reflect.defineMetadata('annotations', annotations, cls); + cls[ CLASS_META_KEY ] = annotations; + + return cls; + + } + + //TypeDecorator.annotations = chainAnnotation; + //TypeDecorator.Class = Class; + + if ( chainFn ) { + chainFn( TypeDecorator ); + } + + return TypeDecorator; + + } + } + + //DecoratorFactory.prototype = Object.create(annotationCls.prototype); + return DecoratorFactory; +} + +export function makeParamDecorator( annotationCls, overrideParamDecorator: Function = null ): any { + + function ParamDecoratorFactory( ...args ): any { + + // create new annotation instance with annotation decorator on proto + const annotationInstance = Object.create( annotationCls.prototype ); + annotationCls.apply( annotationInstance, args ); + + if ( this instanceof annotationCls ) { + + return annotationInstance; + + } else { + + //(ParamDecorator as any).annotation = annotationInstance; + return ParamDecorator; + + } + + /** + * paramDecorators are 2 dimensional arrays + * @param cls + * @param unusedKey + * @param index + * @returns {any} + * @constructor + */ + function ParamDecorator( cls: any, unusedKey: string, index: number ): any { + + // this is special behaviour for non constructor param Injection + if ( isFunction( overrideParamDecorator ) && isPresent( unusedKey ) ) { + + return overrideParamDecorator( annotationInstance, cls, unusedKey, index ); + + } + + //var parameters: any[][] = Reflect.getMetadata('parameters', cls); + var parameters: any[][] = cls[ PARAM_META_KEY ]; + parameters = parameters || []; + + // there might be gaps if some in between parameters do not have annotations. + // we pad with nulls. + while ( parameters.length <= index ) { + parameters.push( null ); + } + + parameters[ index ] = parameters[ index ] || []; + + var annotationsForParam: any[] = parameters[ index ]; + annotationsForParam.push( annotationInstance ); + + //Reflect.defineMetadata('parameters', parameters, cls); + cls[ PARAM_META_KEY ] = parameters; + + return cls; + + } + + } + + //ParamDecoratorFactory.prototype = Object.create(annotationCls.prototype); + + return ParamDecoratorFactory; + +} + +export function makePropDecorator( decoratorCls ): any { + + function PropDecoratorFactory( ...args ): any { + + var decoratorInstance = Object.create( decoratorCls.prototype ); + decoratorCls.apply( decoratorInstance, args ); + + if ( this instanceof decoratorCls ) { + + return decoratorInstance; + + } else { + + return function PropDecorator( target: any, name: string ) { + + + //var meta = Reflect.getOwnMetadata('propMetadata', target.constructor); + var meta = target.constructor[ PROP_META_KEY ]; + + meta = meta || {}; + meta[ name ] = meta[ name ] || []; + meta[ name ].unshift( decoratorInstance ); + + + + //Reflect.defineMetadata('propMetadata', meta, target.constructor); + target.constructor[ PROP_META_KEY ] = meta; + + }; + + } + + } + + //PropDecoratorFactory.prototype = Object.create(decoratorCls.prototype); + + return PropDecoratorFactory; + +} diff --git a/test/util/decorators.spec.ts b/test/util/decorators.spec.ts new file mode 100644 index 0000000..0c1c9f1 --- /dev/null +++ b/test/util/decorators.spec.ts @@ -0,0 +1,74 @@ +import {expect} from 'chai'; +import {extractAnnotation,extractParameter,extractProperty,makePropDecorator} from '../../src/util/decorators'; +import {Injectable} from "../../src/di/decorators"; +import {Inject} from "../../src/di/decorators"; +import {InjectableMetadata} from "../../src/di/metadata"; +import {Host} from "../../src/di/decorators"; +import {HostMetadata} from "../../src/di/metadata"; +import {assign} from "../../src/facade/lang"; +import {InjectMetadata} from "../../src/di/metadata"; + +describe( `util/decorators`, ()=> { + + describe( `extraction helpers`, ()=> { + + it( `should extract class annotation if present`, ()=> { + + @Injectable() + class Test{} + + const actual = extractAnnotation(Test); + const expected = [InjectableMetadata.prototype]; + + expect(actual).to.deep.equal(expected); + + } ); + it( `should extract class property metadata if present`, ()=> { + + class FooMetadata{ + toString(): string { return `@Foo()`; } + } + const Foo = makePropDecorator(FooMetadata); + + class Test{ + @Foo() jedi: string; + + constructor(){ + this.jedi = 'Obi-wan Kenobi'; + } + } + + const actual = extractProperty(Test); + const expected = { jedi: [ FooMetadata.prototype ] }; + + expect(actual).to.deep.equal(expected); + + } ); + it( `should extract constructor params metadata if present`, ()=> { + + function _createProto( Type, props ) { + const instance = Object.create(Type.prototype); + return assign(instance,props); + } + class Test{ + constructor( + @Inject('$http') private $http: ng.IHttpService, + @Inject('$log') private $log: ng.ILogService, + @Host() @Inject('ngModel') ngModel: ng.INgModelController + ){} + } + + const actual = extractParameter(Test); + const expected = [ + [_createProto(InjectMetadata,{token:'$http'})], + [_createProto(InjectMetadata,{token:'$log'})], + [_createProto(InjectMetadata,{token:'ngModel'}),_createProto(HostMetadata,null)] + ]; + + expect(actual).to.deep.equal(expected); + + } ); + + } ); + +} );