From c3ae34f066e901f0825ad9f74af7f1181370bcc7 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 12 Jun 2015 23:51:42 -0700 Subject: [PATCH] feat: support decorator chaining and class creation in ES5 Closes #2534 --- .../src/core/annotations/annotations.ts | 2 + .../src/core/annotations/decorators.ts | 42 ++++- modules/angular2/src/core/annotations/view.ts | 4 +- .../src/core/annotations_impl/annotations.ts | 51 +++--- .../src/core/annotations_impl/view.ts | 13 +- modules/angular2/src/util/decorators.ts | 165 ++++++++++++++---- .../core/annotations/decorators_spec.dart | 5 + .../test/core/annotations/decorators_spec.ts | 28 +++ .../test/core/compiler/compiler_spec.ts | 8 +- .../test/reflection/reflector_common.ts | 2 +- .../angular2/test/util/decorators_spec.dart | 5 + modules/angular2/test/util/decorators_spec.ts | 121 +++++++++++++ 12 files changed, 371 insertions(+), 75 deletions(-) create mode 100644 modules/angular2/test/core/annotations/decorators_spec.dart create mode 100644 modules/angular2/test/core/annotations/decorators_spec.ts create mode 100644 modules/angular2/test/util/decorators_spec.dart create mode 100644 modules/angular2/test/util/decorators_spec.ts diff --git a/modules/angular2/src/core/annotations/annotations.ts b/modules/angular2/src/core/annotations/annotations.ts index 7f0a1b8873..9663e31cc5 100644 --- a/modules/angular2/src/core/annotations/annotations.ts +++ b/modules/angular2/src/core/annotations/annotations.ts @@ -6,6 +6,8 @@ export { Component as ComponentAnnotation, Directive as DirectiveAnnotation, + ComponentArgs, + DirectiveArgs, onDestroy, onChange, onCheck, diff --git a/modules/angular2/src/core/annotations/decorators.ts b/modules/angular2/src/core/annotations/decorators.ts index 1829f79579..3928b784e0 100644 --- a/modules/angular2/src/core/annotations/decorators.ts +++ b/modules/angular2/src/core/annotations/decorators.ts @@ -1,5 +1,10 @@ -import {ComponentAnnotation, DirectiveAnnotation} from './annotations'; -import {ViewAnnotation} from './view'; +import { + ComponentAnnotation, + DirectiveAnnotation, + ComponentArgs, + DirectiveArgs +} from './annotations'; +import {ViewAnnotation, ViewArgs} from './view'; import { SelfAnnotation, ParentAnnotation, @@ -7,14 +12,39 @@ import { UnboundedAnnotation } from './visibility'; import {AttributeAnnotation, QueryAnnotation} from './di'; -import {makeDecorator, makeParamDecorator} from '../../util/decorators'; +import {makeDecorator, makeParamDecorator, TypeDecorator, Class} from '../../util/decorators'; +import {Type} from 'angular2/src/facade/lang'; + +export interface DirectiveTypeDecorator extends TypeDecorator {} + +export interface ComponentTypeDecorator extends TypeDecorator { + View(obj: ViewArgs): ViewTypeDecorator; +} + +export interface ViewTypeDecorator extends TypeDecorator { View(obj: ViewArgs): ViewTypeDecorator } + +export interface Directive { + (obj: any): DirectiveTypeDecorator; + new (obj: DirectiveAnnotation): DirectiveAnnotation; +} + +export interface Component { + (obj: any): ComponentTypeDecorator; + new (obj: ComponentAnnotation): ComponentAnnotation; +} + +export interface View { + (obj: ViewArgs): ViewTypeDecorator; + new (obj: ViewArgs): ViewAnnotation; +} + /* from annotations */ -export var Component = makeDecorator(ComponentAnnotation); -export var Directive = makeDecorator(DirectiveAnnotation); +export var Component = makeDecorator(ComponentAnnotation, (fn: any) => fn.View = View); +export var Directive = makeDecorator(DirectiveAnnotation); /* from view */ -export var View = makeDecorator(ViewAnnotation); +export var View = makeDecorator(ViewAnnotation, (fn: any) => fn.View = View); /* from visibility */ export var Self = makeParamDecorator(SelfAnnotation); diff --git a/modules/angular2/src/core/annotations/view.ts b/modules/angular2/src/core/annotations/view.ts index 8aee85b989..39e2ae0ac6 100644 --- a/modules/angular2/src/core/annotations/view.ts +++ b/modules/angular2/src/core/annotations/view.ts @@ -1,3 +1 @@ -export { - View as ViewAnnotation, -} from '../annotations_impl/view'; +export {View as ViewAnnotation, ViewArgs} from '../annotations_impl/view'; diff --git a/modules/angular2/src/core/annotations_impl/annotations.ts b/modules/angular2/src/core/annotations_impl/annotations.ts index 3d7ede9efd..7ea8c0bd05 100644 --- a/modules/angular2/src/core/annotations_impl/annotations.ts +++ b/modules/angular2/src/core/annotations_impl/annotations.ts @@ -787,16 +787,7 @@ export class Directive extends Injectable { constructor({ selector, properties, events, host, lifecycle, hostInjector, exportAs, compileChildren = true, - }: { - selector?: string, - properties?: List, - events?: List, - host?: StringMap, - lifecycle?: List, - hostInjector?: List, - exportAs?: string, - compileChildren?: boolean - } = {}) { + }: ComponentArgs = {}) { super(); this.selector = selector; this.properties = properties; @@ -809,6 +800,17 @@ export class Directive extends Injectable { } } +export interface ComponentArgs { + selector?: string; + properties?: List; + events?: List; + host?: StringMap; + lifecycle?: List; + hostInjector?: List; + exportAs?: string; + compileChildren?: boolean; +} + /** * Declare reusable UI building blocks for an application. * @@ -1007,19 +1009,8 @@ export class Component extends Directive { viewInjector: List; constructor({selector, properties, events, host, exportAs, appInjector, lifecycle, hostInjector, - viewInjector, changeDetection = DEFAULT, compileChildren = true}: { - selector?: string, - properties?: List, - events?: List, - host?: StringMap, - exportAs?: string, - appInjector?: List, - lifecycle?: List, - hostInjector?: List, - viewInjector?: List, - changeDetection?: string, - compileChildren?: boolean - } = {}) { + viewInjector, changeDetection = DEFAULT, + compileChildren = true}: DirectiveArgs = {}) { super({ selector: selector, properties: properties, @@ -1036,6 +1027,20 @@ export class Component extends Directive { this.viewInjector = viewInjector; } } +export interface DirectiveArgs { + selector?: string; + properties?: List; + events?: List; + host?: StringMap; + exportAs?: string; + appInjector?: List; + lifecycle?: List; + hostInjector?: List; + viewInjector?: List; + changeDetection?: string; + compileChildren?: boolean; +} + /** * Lifecycle events are guaranteed to be called in the following order: diff --git a/modules/angular2/src/core/annotations_impl/view.ts b/modules/angular2/src/core/annotations_impl/view.ts index d6f271589d..f4b5bd5605 100644 --- a/modules/angular2/src/core/annotations_impl/view.ts +++ b/modules/angular2/src/core/annotations_impl/view.ts @@ -82,15 +82,16 @@ export class View { */ renderer: string; - constructor({templateUrl, template, directives, renderer}: { - templateUrl?: string, - template?: string, - directives?: List>, - renderer?: string - } = {}) { + constructor({templateUrl, template, directives, renderer}: ViewArgs = {}) { this.templateUrl = templateUrl; this.template = template; this.directives = directives; this.renderer = renderer; } } +export interface ViewArgs { + templateUrl?: string; + template?: string; + directives?: List>; + renderer?: string; +} diff --git a/modules/angular2/src/util/decorators.ts b/modules/angular2/src/util/decorators.ts index 330fec99e9..a6f53296cf 100644 --- a/modules/angular2/src/util/decorators.ts +++ b/modules/angular2/src/util/decorators.ts @@ -1,48 +1,149 @@ -import {global} from 'angular2/src/facade/lang'; +import {global, Type, isFunction, stringify} from 'angular2/src/facade/lang'; -export function makeDecorator(annotationCls) { - return function(...args) { - var Reflect = global.Reflect; - if (!(Reflect && Reflect.getMetadata)) { - throw 'reflect-metadata shim is required when using class decorators'; +export interface ClassDefinition { + extends?: Type; + constructor: (Function | Array); +} + +export interface TypeDecorator { + (cls: any): any; + annotations: Array; + Class(obj: ClassDefinition): Type; +} + +function extractAnnotation(annotation: any) { + if (isFunction(annotation) && annotation.hasOwnProperty('annotation')) { + // it is a decorator, extract annotation + annotation = annotation.annotation; + } + return annotation; +} + +function applyParams(fnOrArray: (Function | Array), key: string): Function { + if (fnOrArray === Object || fnOrArray === String || fnOrArray === Function || + fnOrArray === Number || fnOrArray === Array) { + throw new Error(`Can not use native ${stringify(fnOrArray)} as constructor`); + } + if (isFunction(fnOrArray)) { + return fnOrArray; + } else if (fnOrArray instanceof Array) { + var annotations: Array = fnOrArray; + var fn: Function = fnOrArray[fnOrArray.length - 1]; + if (!isFunction(fn)) { + throw new Error( + `Last position of Class method array must be Function in key ${key} was '${stringify(fn)}'`); } - var annotationInstance = Object.create(annotationCls.prototype); - annotationCls.apply(annotationInstance, args); - return function(cls) { + var annoLength = annotations.length - 1; + if (annoLength != fn.length) { + throw new Error( + `Number of annotations (${annoLength}) does not match number of arguments (${fn.length}) in the function: ${stringify(fn)}`); + } + var paramsAnnotations: Array> = []; + for (var i = 0, ii = annotations.length - 1; i < ii; i++) { + var paramAnnotations: Array = []; + paramsAnnotations.push(paramAnnotations); + var annotation = annotations[i]; + if (annotation instanceof Array) { + for (var j = 0; j < annotation.length; j++) { + paramAnnotations.push(extractAnnotation(annotation[j])); + } + } else if (isFunction(annotation)) { + paramAnnotations.push(extractAnnotation(annotation)); + } else { + paramAnnotations.push(annotation); + } + } + Reflect.defineMetadata('parameters', paramsAnnotations, fn); + return fn; + } else { + throw new Error( + `Only Function or Array is supported in Class definition for key '${key}' is '${stringify(fnOrArray)}'`); + } +} - var annotations = Reflect.getMetadata('annotations', cls); - annotations = annotations || []; - annotations.push(annotationInstance); - Reflect.defineMetadata('annotations', annotations, cls); - return cls; +export function Class(clsDef: ClassDefinition): Type { + var constructor = applyParams( + clsDef.hasOwnProperty('constructor') ? clsDef.constructor : undefined, 'constructor'); + var proto = constructor.prototype; + if (clsDef.hasOwnProperty('extends')) { + if (isFunction(clsDef.extends)) { + (constructor).prototype = proto = + Object.create((clsDef.extends).prototype); + } else { + throw new Error( + `Class definition 'extends' property must be a constructor function was: ${stringify(clsDef.extends)}`); } } + for (var key in clsDef) { + if (key != 'extends' && key != 'prototype' && clsDef.hasOwnProperty(key)) { + proto[key] = applyParams(clsDef[key], key); + } + } + return constructor; +} + +var Reflect = global.Reflect; +if (!(Reflect && Reflect.getMetadata)) { + throw 'reflect-metadata shim is required when using class decorators'; +} + +export function makeDecorator(annotationCls, chainFn: (fn: Function) => void = null): (...args) => + (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); + var TypeDecorator: TypeDecorator = function TypeDecorator(cls) { + var annotations = Reflect.getMetadata('annotations', cls); + annotations = annotations || []; + annotations.push(annotationInstance); + Reflect.defineMetadata('annotations', annotations, cls); + 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): any { - return function(...args) { - var Reflect = global.Reflect; - if (!(Reflect && Reflect.getMetadata)) { - throw 'reflect-metadata shim is required when using parameter decorators'; - } + function ParamDecoratorFactory(...args) { var annotationInstance = Object.create(annotationCls.prototype); annotationCls.apply(annotationInstance, args); - return function(cls, unusedKey, index) { - var parameters: Array> = Reflect.getMetadata('parameters', cls); - parameters = parameters || []; + if (this instanceof annotationCls) { + return annotationInstance; + } else { + function ParamDecorator(cls, unusedKey, index) { + var parameters: Array> = Reflect.getMetadata('parameters', cls); + 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); + // 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: Array = parameters[index]; + annotationsForParam.push(annotationInstance); + + Reflect.defineMetadata('parameters', parameters, cls); + return cls; } - parameters[index] = parameters[index] || []; - var annotationsForParam: Array = parameters[index]; - annotationsForParam.push(annotationInstance); - - Reflect.defineMetadata('parameters', parameters, cls); - return cls; + (ParamDecorator).annotation = annotationInstance; + return ParamDecorator; } } + ParamDecoratorFactory.prototype = Object.create(annotationCls.prototype); + return ParamDecoratorFactory; } diff --git a/modules/angular2/test/core/annotations/decorators_spec.dart b/modules/angular2/test/core/annotations/decorators_spec.dart new file mode 100644 index 0000000000..29fa75dd2e --- /dev/null +++ b/modules/angular2/test/core/annotations/decorators_spec.dart @@ -0,0 +1,5 @@ +library angular2.test.core.annotations.decorators_dart_spec; + +main() { + // not relavant for dart. +} diff --git a/modules/angular2/test/core/annotations/decorators_spec.ts b/modules/angular2/test/core/annotations/decorators_spec.ts new file mode 100644 index 0000000000..92b33b90f9 --- /dev/null +++ b/modules/angular2/test/core/annotations/decorators_spec.ts @@ -0,0 +1,28 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xit, +} from 'angular2/test_lib'; + +import {Component, View, Directive} from 'angular2/angular2'; + +export function main() { + describe('es5 decorators', () => { + it('should declare directive class', () => { + var MyDirective = Directive({}).Class({constructor: function() { this.works = true; }}); + expect(new MyDirective().works).toEqual(true); + }); + + it('should declare Component class', () => { + var MyComponent = + Component({}).View({}).View({}).Class({constructor: function() { this.works = true; }}); + expect(new MyComponent().works).toEqual(true); + }); + }); +} diff --git a/modules/angular2/test/core/compiler/compiler_spec.ts b/modules/angular2/test/core/compiler/compiler_spec.ts index e3864ea9e9..ddfa2ad6ea 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.ts +++ b/modules/angular2/test/core/compiler/compiler_spec.ts @@ -451,17 +451,17 @@ function createRenderViewportElementBinder(nestedProtoView) { class MainComponent { } -@Component() +@Component({selector: 'nested'}) class NestedComponent { } class RecursiveComponent {} -@Component() +@Component({selector: 'some-dynamic'}) class SomeDynamicComponentDirective { } -@Directive() +@Directive({selector: 'some'}) class SomeDirective { } @@ -481,7 +481,7 @@ class DirectiveWithProperties { class DirectiveWithBind { } -@Directive() +@Directive({selector: 'directive-with-accts'}) class DirectiveWithAttributes { constructor(@Attribute('someAttr') someAttr: String) {} } diff --git a/modules/angular2/test/reflection/reflector_common.ts b/modules/angular2/test/reflection/reflector_common.ts index c419d0fa1f..ea6d16b697 100644 --- a/modules/angular2/test/reflection/reflector_common.ts +++ b/modules/angular2/test/reflection/reflector_common.ts @@ -21,4 +21,4 @@ export function paramDecorator(value) { } export var ClassDecorator = makeDecorator(ClassDecoratorImpl); -export var ParamDecorator = makeParamDecorator(ParamDecoratorImpl); \ No newline at end of file +export var ParamDecorator = makeParamDecorator(ParamDecoratorImpl); diff --git a/modules/angular2/test/util/decorators_spec.dart b/modules/angular2/test/util/decorators_spec.dart new file mode 100644 index 0000000000..3ac4e37048 --- /dev/null +++ b/modules/angular2/test/util/decorators_spec.dart @@ -0,0 +1,5 @@ +library angular2.test.util.decorators_dart_spec; + +main() { + // not relavant for dart. +} diff --git a/modules/angular2/test/util/decorators_spec.ts b/modules/angular2/test/util/decorators_spec.ts new file mode 100644 index 0000000000..a489915a27 --- /dev/null +++ b/modules/angular2/test/util/decorators_spec.ts @@ -0,0 +1,121 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xit, +} from 'angular2/test_lib'; + +import {makeDecorator, makeParamDecorator, Class} from 'angular2/src/util/decorators'; +import {global} from 'angular2/src/facade/lang'; +import {Inject} from 'angular2/angular2'; +import {reflector} from 'angular2/src/reflection/reflection'; + +class TestAnnotation { + constructor(public arg: any) {} +} + +class TerminalAnnotation { + terminal = true; +} + +export function main() { + var Reflect = global.Reflect; + + var TerminalDecorator = makeDecorator(TerminalAnnotation); + var TestDecorator = makeDecorator(TestAnnotation, (fn: any) => fn.Terminal = TerminalDecorator); + var TestParamDecorator = makeParamDecorator(TestAnnotation); + + describe('decorators', () => { + it('shoulld invoke as decorator', () => { + function Type(){}; + TestDecorator({marker: 'WORKS'})(Type); + var annotations = Reflect.getMetadata('annotations', Type); + expect(annotations[0].arg.marker).toEqual('WORKS'); + }); + + it('should invoke as new', () => { + var annotation = new (TestDecorator)({marker: 'WORKS'}); + expect(annotation instanceof TestAnnotation).toEqual(true); + expect(annotation.arg.marker).toEqual('WORKS'); + }); + + it('should invoke as chain', () => { + var chain: any = TestDecorator({marker: 'WORKS'}); + expect(typeof chain.Terminal).toEqual('function'); + chain = chain.Terminal(); + expect(chain.annotations[0] instanceof TestAnnotation).toEqual(true); + expect(chain.annotations[0].arg.marker).toEqual('WORKS'); + expect(chain.annotations[1] instanceof TerminalAnnotation).toEqual(true); + }); + + describe('Class', () => { + it('should create a class', () => { + var i0, i1; + var MyClass = Class({ + extends: Class({ + constructor: function() {}, + extendWorks: function() { return 'extend ' + this.arg; } + }), + constructor: [String, function(arg) { this.arg = arg; }], + methodA: [i0 = new Inject(String), [i1 = Inject(String), Number], function(a, b) {}], + works: function() { return this.arg; }, + prototype: 'IGNORE' + }); + var obj: any = new MyClass('WORKS'); + expect(obj.arg).toEqual('WORKS'); + expect(obj.works()).toEqual('WORKS'); + expect(obj.extendWorks()).toEqual('extend WORKS'); + expect(reflector.parameters(MyClass)).toEqual([[String]]); + expect(reflector.parameters(obj.methodA)).toEqual([[i0], [i1.annotation, Number]]); + + var proto = (MyClass).prototype; + expect(proto.extends).toEqual(undefined); + expect(proto.prototype).toEqual(undefined); + }); + + describe('errors', () => { + it('should ensure that last constructor is required', () => { + expect(() => { (Class)({}); }) + .toThrowError( + "Only Function or Array is supported in Class definition for key 'constructor' is 'undefined'"); + }); + + + it('should ensure that we dont accidently patch native objects', () => { + expect(() => { (Class)({constructor: Object}); }) + .toThrowError("Can not use native Object as constructor"); + }); + + + it('should ensure that last possition is function', () => { + expect(() => {Class({constructor: []})}) + .toThrowError( + "Last position of Class method array must be Function in key constructor was 'undefined'"); + }); + + it('should ensure that annotation count matches paramaters count', () => { + expect(() => {Class({constructor: [String, function MyType() {}]})}) + .toThrowError( + "Number of annotations (1) does not match number of arguments (0) in the function: MyType"); + }); + + it('should ensure that only Function|Arrays are supported', () => { + expect(() => { Class({constructor: function() {}, method: 'non_function'}); }) + .toThrowError( + "Only Function or Array is supported in Class definition for key 'method' is 'non_function'"); + }); + + it('should ensure that extends is a Function', () => { + expect(() => {(Class)({extends: 'non_type', constructor: function() {}})}) + .toThrowError( + "Class definition 'extends' property must be a constructor function was: non_type"); + }); + }); + }); + }); +}