feat: support decorator chaining and class creation in ES5

Closes #2534
This commit is contained in:
Misko Hevery 2015-06-12 23:51:42 -07:00
parent 4f581671dc
commit c3ae34f066
12 changed files with 371 additions and 75 deletions

View File

@ -6,6 +6,8 @@
export {
Component as ComponentAnnotation,
Directive as DirectiveAnnotation,
ComponentArgs,
DirectiveArgs,
onDestroy,
onChange,
onCheck,

View File

@ -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 = <Component>makeDecorator(ComponentAnnotation, (fn: any) => fn.View = View);
export var Directive = <Directive>makeDecorator(DirectiveAnnotation);
/* from view */
export var View = makeDecorator(ViewAnnotation);
export var View = <View>makeDecorator(ViewAnnotation, (fn: any) => fn.View = View);
/* from visibility */
export var Self = makeParamDecorator(SelfAnnotation);

View File

@ -1,3 +1 @@
export {
View as ViewAnnotation,
} from '../annotations_impl/view';
export {View as ViewAnnotation, ViewArgs} from '../annotations_impl/view';

View File

@ -787,16 +787,7 @@ export class Directive extends Injectable {
constructor({
selector, properties, events, host, lifecycle, hostInjector, exportAs,
compileChildren = true,
}: {
selector?: string,
properties?: List<string>,
events?: List<string>,
host?: StringMap<string, string>,
lifecycle?: List<LifecycleEvent>,
hostInjector?: List<any>,
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<string>;
events?: List<string>;
host?: StringMap<string, string>;
lifecycle?: List<LifecycleEvent>;
hostInjector?: List<any>;
exportAs?: string;
compileChildren?: boolean;
}
/**
* Declare reusable UI building blocks for an application.
*
@ -1007,19 +1009,8 @@ export class Component extends Directive {
viewInjector: List<any>;
constructor({selector, properties, events, host, exportAs, appInjector, lifecycle, hostInjector,
viewInjector, changeDetection = DEFAULT, compileChildren = true}: {
selector?: string,
properties?: List<string>,
events?: List<string>,
host?: StringMap<string, string>,
exportAs?: string,
appInjector?: List<any>,
lifecycle?: List<LifecycleEvent>,
hostInjector?: List<any>,
viewInjector?: List<any>,
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<string>;
events?: List<string>;
host?: StringMap<string, string>;
exportAs?: string;
appInjector?: List<any>;
lifecycle?: List<LifecycleEvent>;
hostInjector?: List<any>;
viewInjector?: List<any>;
changeDetection?: string;
compileChildren?: boolean;
}
/**
* Lifecycle events are guaranteed to be called in the following order:

View File

@ -82,15 +82,16 @@ export class View {
*/
renderer: string;
constructor({templateUrl, template, directives, renderer}: {
templateUrl?: string,
template?: string,
directives?: List<Type | any | List<any>>,
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<Type | any | List<any>>;
renderer?: string;
}

View File

@ -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<any>);
}
export interface TypeDecorator {
(cls: any): any;
annotations: Array<any>;
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<any>), 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 <Function>fnOrArray;
} else if (fnOrArray instanceof Array) {
var annotations: Array<any> = 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<Array<any>> = [];
for (var i = 0, ii = annotations.length - 1; i < ii; i++) {
var paramAnnotations: Array<any> = [];
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)) {
(<Function>constructor).prototype = proto =
Object.create((<Function>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 <Type>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 (<any>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 = <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<Array<any>> = Reflect.getMetadata('parameters', cls);
parameters = parameters || [];
if (this instanceof annotationCls) {
return annotationInstance;
} else {
function ParamDecorator(cls, unusedKey, index) {
var parameters: Array<Array<any>> = 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<any> = parameters[index];
annotationsForParam.push(annotationInstance);
Reflect.defineMetadata('parameters', parameters, cls);
return cls;
}
parameters[index] = parameters[index] || [];
var annotationsForParam: Array<any> = parameters[index];
annotationsForParam.push(annotationInstance);
Reflect.defineMetadata('parameters', parameters, cls);
return cls;
(<any>ParamDecorator).annotation = annotationInstance;
return ParamDecorator;
}
}
ParamDecoratorFactory.prototype = Object.create(annotationCls.prototype);
return ParamDecoratorFactory;
}

View File

@ -0,0 +1,5 @@
library angular2.test.core.annotations.decorators_dart_spec;
main() {
// not relavant for dart.
}

View File

@ -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);
});
});
}

View File

@ -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) {}
}

View File

@ -21,4 +21,4 @@ export function paramDecorator(value) {
}
export var ClassDecorator = makeDecorator(ClassDecoratorImpl);
export var ParamDecorator = makeParamDecorator(ParamDecoratorImpl);
export var ParamDecorator = makeParamDecorator(ParamDecoratorImpl);

View File

@ -0,0 +1,5 @@
library angular2.test.util.decorators_dart_spec;
main() {
// not relavant for dart.
}

View File

@ -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 (<any>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 = (<Function>MyClass).prototype;
expect(proto.extends).toEqual(undefined);
expect(proto.prototype).toEqual(undefined);
});
describe('errors', () => {
it('should ensure that last constructor is required', () => {
expect(() => { (<Function>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(() => { (<Function>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(() => {(<Function>Class)({extends: 'non_type', constructor: function() {}})})
.toThrowError(
"Class definition 'extends' property must be a constructor function was: non_type");
});
});
});
});
}