Skip to content

Commit c3ae34f

Browse files
committed
feat: support decorator chaining and class creation in ES5
Closes angular#2534
1 parent 4f58167 commit c3ae34f

File tree

12 files changed

+372
-76
lines changed

12 files changed

+372
-76
lines changed

modules/angular2/src/core/annotations/annotations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
export {
77
Component as ComponentAnnotation,
88
Directive as DirectiveAnnotation,
9+
ComponentArgs,
10+
DirectiveArgs,
911
onDestroy,
1012
onChange,
1113
onCheck,

modules/angular2/src/core/annotations/decorators.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
1-
import {ComponentAnnotation, DirectiveAnnotation} from './annotations';
2-
import {ViewAnnotation} from './view';
1+
import {
2+
ComponentAnnotation,
3+
DirectiveAnnotation,
4+
ComponentArgs,
5+
DirectiveArgs
6+
} from './annotations';
7+
import {ViewAnnotation, ViewArgs} from './view';
38
import {
49
SelfAnnotation,
510
ParentAnnotation,
611
AncestorAnnotation,
712
UnboundedAnnotation
813
} from './visibility';
914
import {AttributeAnnotation, QueryAnnotation} from './di';
10-
import {makeDecorator, makeParamDecorator} from '../../util/decorators';
15+
import {makeDecorator, makeParamDecorator, TypeDecorator, Class} from '../../util/decorators';
16+
import {Type} from 'angular2/src/facade/lang';
17+
18+
export interface DirectiveTypeDecorator extends TypeDecorator {}
19+
20+
export interface ComponentTypeDecorator extends TypeDecorator {
21+
View(obj: ViewArgs): ViewTypeDecorator;
22+
}
23+
24+
export interface ViewTypeDecorator extends TypeDecorator { View(obj: ViewArgs): ViewTypeDecorator }
25+
26+
export interface Directive {
27+
(obj: any): DirectiveTypeDecorator;
28+
new (obj: DirectiveAnnotation): DirectiveAnnotation;
29+
}
30+
31+
export interface Component {
32+
(obj: any): ComponentTypeDecorator;
33+
new (obj: ComponentAnnotation): ComponentAnnotation;
34+
}
35+
36+
export interface View {
37+
(obj: ViewArgs): ViewTypeDecorator;
38+
new (obj: ViewArgs): ViewAnnotation;
39+
}
40+
1141

1242
/* from annotations */
13-
export var Component = makeDecorator(ComponentAnnotation);
14-
export var Directive = makeDecorator(DirectiveAnnotation);
43+
export var Component = <Component>makeDecorator(ComponentAnnotation, (fn: any) => fn.View = View);
44+
export var Directive = <Directive>makeDecorator(DirectiveAnnotation);
1545

1646
/* from view */
17-
export var View = makeDecorator(ViewAnnotation);
47+
export var View = <View>makeDecorator(ViewAnnotation, (fn: any) => fn.View = View);
1848

1949
/* from visibility */
2050
export var Self = makeParamDecorator(SelfAnnotation);
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
export {
2-
View as ViewAnnotation,
3-
} from '../annotations_impl/view';
1+
export {View as ViewAnnotation, ViewArgs} from '../annotations_impl/view';

modules/angular2/src/core/annotations_impl/annotations.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -787,16 +787,7 @@ export class Directive extends Injectable {
787787
constructor({
788788
selector, properties, events, host, lifecycle, hostInjector, exportAs,
789789
compileChildren = true,
790-
}: {
791-
selector?: string,
792-
properties?: List<string>,
793-
events?: List<string>,
794-
host?: StringMap<string, string>,
795-
lifecycle?: List<LifecycleEvent>,
796-
hostInjector?: List<any>,
797-
exportAs?: string,
798-
compileChildren?: boolean
799-
} = {}) {
790+
}: ComponentArgs = {}) {
800791
super();
801792
this.selector = selector;
802793
this.properties = properties;
@@ -809,6 +800,17 @@ export class Directive extends Injectable {
809800
}
810801
}
811802

803+
export interface ComponentArgs {
804+
selector?: string;
805+
properties?: List<string>;
806+
events?: List<string>;
807+
host?: StringMap<string, string>;
808+
lifecycle?: List<LifecycleEvent>;
809+
hostInjector?: List<any>;
810+
exportAs?: string;
811+
compileChildren?: boolean;
812+
}
813+
812814
/**
813815
* Declare reusable UI building blocks for an application.
814816
*
@@ -1007,19 +1009,8 @@ export class Component extends Directive {
10071009
viewInjector: List<any>;
10081010

10091011
constructor({selector, properties, events, host, exportAs, appInjector, lifecycle, hostInjector,
1010-
viewInjector, changeDetection = DEFAULT, compileChildren = true}: {
1011-
selector?: string,
1012-
properties?: List<string>,
1013-
events?: List<string>,
1014-
host?: StringMap<string, string>,
1015-
exportAs?: string,
1016-
appInjector?: List<any>,
1017-
lifecycle?: List<LifecycleEvent>,
1018-
hostInjector?: List<any>,
1019-
viewInjector?: List<any>,
1020-
changeDetection?: string,
1021-
compileChildren?: boolean
1022-
} = {}) {
1012+
viewInjector, changeDetection = DEFAULT,
1013+
compileChildren = true}: DirectiveArgs = {}) {
10231014
super({
10241015
selector: selector,
10251016
properties: properties,
@@ -1036,6 +1027,20 @@ export class Component extends Directive {
10361027
this.viewInjector = viewInjector;
10371028
}
10381029
}
1030+
export interface DirectiveArgs {
1031+
selector?: string;
1032+
properties?: List<string>;
1033+
events?: List<string>;
1034+
host?: StringMap<string, string>;
1035+
exportAs?: string;
1036+
appInjector?: List<any>;
1037+
lifecycle?: List<LifecycleEvent>;
1038+
hostInjector?: List<any>;
1039+
viewInjector?: List<any>;
1040+
changeDetection?: string;
1041+
compileChildren?: boolean;
1042+
}
1043+
10391044

10401045
/**
10411046
* Lifecycle events are guaranteed to be called in the following order:

modules/angular2/src/core/annotations_impl/view.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,16 @@ export class View {
8282
*/
8383
renderer: string;
8484

85-
constructor({templateUrl, template, directives, renderer}: {
86-
templateUrl?: string,
87-
template?: string,
88-
directives?: List<Type | any | List<any>>,
89-
renderer?: string
90-
} = {}) {
85+
constructor({templateUrl, template, directives, renderer}: ViewArgs = {}) {
9186
this.templateUrl = templateUrl;
9287
this.template = template;
9388
this.directives = directives;
9489
this.renderer = renderer;
9590
}
9691
}
92+
export interface ViewArgs {
93+
templateUrl?: string;
94+
template?: string;
95+
directives?: List<Type | any | List<any>>;
96+
renderer?: string;
97+
}
Lines changed: 134 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,149 @@
1-
import {global} from 'angular2/src/facade/lang';
1+
import {global, Type, isFunction, stringify} from 'angular2/src/facade/lang';
22

3-
export function makeDecorator(annotationCls) {
4-
return function(...args) {
5-
var Reflect = global.Reflect;
6-
if (!(Reflect && Reflect.getMetadata)) {
7-
throw 'reflect-metadata shim is required when using class decorators';
3+
export interface ClassDefinition {
4+
extends?: Type;
5+
constructor: (Function | Array<any>);
6+
}
7+
8+
export interface TypeDecorator {
9+
(cls: any): any;
10+
annotations: Array<any>;
11+
Class(obj: ClassDefinition): Type;
12+
}
13+
14+
function extractAnnotation(annotation: any) {
15+
if (isFunction(annotation) && annotation.hasOwnProperty('annotation')) {
16+
// it is a decorator, extract annotation
17+
annotation = annotation.annotation;
18+
}
19+
return annotation;
20+
}
21+
22+
function applyParams(fnOrArray: (Function | Array<any>), key: string): Function {
23+
if (fnOrArray === Object || fnOrArray === String || fnOrArray === Function ||
24+
fnOrArray === Number || fnOrArray === Array) {
25+
throw new Error(`Can not use native ${stringify(fnOrArray)} as constructor`);
26+
}
27+
if (isFunction(fnOrArray)) {
28+
return <Function>fnOrArray;
29+
} else if (fnOrArray instanceof Array) {
30+
var annotations: Array<any> = fnOrArray;
31+
var fn: Function = fnOrArray[fnOrArray.length - 1];
32+
if (!isFunction(fn)) {
33+
throw new Error(
34+
`Last position of Class method array must be Function in key ${key} was '${stringify(fn)}'`);
835
}
9-
var annotationInstance = Object.create(annotationCls.prototype);
10-
annotationCls.apply(annotationInstance, args);
11-
return function(cls) {
36+
var annoLength = annotations.length - 1;
37+
if (annoLength != fn.length) {
38+
throw new Error(
39+
`Number of annotations (${annoLength}) does not match number of arguments (${fn.length}) in the function: ${stringify(fn)}`);
40+
}
41+
var paramsAnnotations: Array<Array<any>> = [];
42+
for (var i = 0, ii = annotations.length - 1; i < ii; i++) {
43+
var paramAnnotations: Array<any> = [];
44+
paramsAnnotations.push(paramAnnotations);
45+
var annotation = annotations[i];
46+
if (annotation instanceof Array) {
47+
for (var j = 0; j < annotation.length; j++) {
48+
paramAnnotations.push(extractAnnotation(annotation[j]));
49+
}
50+
} else if (isFunction(annotation)) {
51+
paramAnnotations.push(extractAnnotation(annotation));
52+
} else {
53+
paramAnnotations.push(annotation);
54+
}
55+
}
56+
Reflect.defineMetadata('parameters', paramsAnnotations, fn);
57+
return fn;
58+
} else {
59+
throw new Error(
60+
`Only Function or Array is supported in Class definition for key '${key}' is '${stringify(fnOrArray)}'`);
61+
}
62+
}
1263

13-
var annotations = Reflect.getMetadata('annotations', cls);
14-
annotations = annotations || [];
15-
annotations.push(annotationInstance);
16-
Reflect.defineMetadata('annotations', annotations, cls);
17-
return cls;
64+
export function Class(clsDef: ClassDefinition): Type {
65+
var constructor = applyParams(
66+
clsDef.hasOwnProperty('constructor') ? clsDef.constructor : undefined, 'constructor');
67+
var proto = constructor.prototype;
68+
if (clsDef.hasOwnProperty('extends')) {
69+
if (isFunction(clsDef.extends)) {
70+
(<Function>constructor).prototype = proto =
71+
Object.create((<Function>clsDef.extends).prototype);
72+
} else {
73+
throw new Error(
74+
`Class definition 'extends' property must be a constructor function was: ${stringify(clsDef.extends)}`);
75+
}
76+
}
77+
for (var key in clsDef) {
78+
if (key != 'extends' && key != 'prototype' && clsDef.hasOwnProperty(key)) {
79+
proto[key] = applyParams(clsDef[key], key);
1880
}
1981
}
82+
return <Type>constructor;
2083
}
2184

22-
export function makeParamDecorator(annotationCls): any {
23-
return function(...args) {
24-
var Reflect = global.Reflect;
25-
if (!(Reflect && Reflect.getMetadata)) {
26-
throw 'reflect-metadata shim is required when using parameter decorators';
85+
var Reflect = global.Reflect;
86+
if (!(Reflect && Reflect.getMetadata)) {
87+
throw 'reflect-metadata shim is required when using class decorators';
88+
}
89+
90+
export function makeDecorator(annotationCls, chainFn: (fn: Function) => void = null): (...args) =>
91+
(cls: any) => any {
92+
function DecoratorFactory(objOrType): (cls: any) => any {
93+
var annotationInstance = new (<any>annotationCls)(objOrType);
94+
if (this instanceof annotationCls) {
95+
return annotationInstance;
96+
} else {
97+
var chainAnnotation = isFunction(this) && this.annotations instanceof Array ?
98+
this.annotations :
99+
[];
100+
chainAnnotation.push(annotationInstance);
101+
var TypeDecorator: TypeDecorator = <TypeDecorator>function TypeDecorator(cls) {
102+
var annotations = Reflect.getMetadata('annotations', cls);
103+
annotations = annotations || [];
104+
annotations.push(annotationInstance);
105+
Reflect.defineMetadata('annotations', annotations, cls);
106+
return cls;
107+
};
108+
TypeDecorator.annotations = chainAnnotation;
109+
TypeDecorator.Class = Class;
110+
if (chainFn) chainFn(TypeDecorator);
111+
return TypeDecorator;
27112
}
113+
}
114+
DecoratorFactory.prototype = Object.create(annotationCls.prototype);
115+
return DecoratorFactory;
116+
}
117+
118+
export function makeParamDecorator(annotationCls): any {
119+
function ParamDecoratorFactory(...args) {
28120
var annotationInstance = Object.create(annotationCls.prototype);
29121
annotationCls.apply(annotationInstance, args);
30-
return function(cls, unusedKey, index) {
31-
var parameters: Array<Array<any>> = Reflect.getMetadata('parameters', cls);
32-
parameters = parameters || [];
33-
34-
// there might be gaps if some in between parameters do not have annotations.
35-
// we pad with nulls.
36-
while (parameters.length <= index) {
37-
parameters.push(null);
38-
}
122+
if (this instanceof annotationCls) {
123+
return annotationInstance;
124+
} else {
125+
function ParamDecorator(cls, unusedKey, index) {
126+
var parameters: Array<Array<any>> = Reflect.getMetadata('parameters', cls);
127+
parameters = parameters || [];
39128

40-
parameters[index] = parameters[index] || [];
41-
var annotationsForParam: Array<any> = parameters[index];
42-
annotationsForParam.push(annotationInstance);
129+
// there might be gaps if some in between parameters do not have annotations.
130+
// we pad with nulls.
131+
while (parameters.length <= index) {
132+
parameters.push(null);
133+
}
134+
135+
parameters[index] = parameters[index] || [];
136+
var annotationsForParam: Array<any> = parameters[index];
137+
annotationsForParam.push(annotationInstance);
138+
139+
Reflect.defineMetadata('parameters', parameters, cls);
140+
return cls;
141+
}
43142

44-
Reflect.defineMetadata('parameters', parameters, cls);
45-
return cls;
143+
(<any>ParamDecorator).annotation = annotationInstance;
144+
return ParamDecorator;
46145
}
47146
}
147+
ParamDecoratorFactory.prototype = Object.create(annotationCls.prototype);
148+
return ParamDecoratorFactory;
48149
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
library angular2.test.core.annotations.decorators_dart_spec;
2+
3+
main() {
4+
// not relavant for dart.
5+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
AsyncTestCompleter,
3+
beforeEach,
4+
ddescribe,
5+
describe,
6+
expect,
7+
iit,
8+
inject,
9+
it,
10+
xit,
11+
} from 'angular2/test_lib';
12+
13+
import {Component, View, Directive} from 'angular2/angular2';
14+
15+
export function main() {
16+
describe('es5 decorators', () => {
17+
it('should declare directive class', () => {
18+
var MyDirective = Directive({}).Class({constructor: function() { this.works = true; }});
19+
expect(new MyDirective().works).toEqual(true);
20+
});
21+
22+
it('should declare Component class', () => {
23+
var MyComponent =
24+
Component({}).View({}).View({}).Class({constructor: function() { this.works = true; }});
25+
expect(new MyComponent().works).toEqual(true);
26+
});
27+
});
28+
}

0 commit comments

Comments
 (0)