feat: support decorator chaining and class creation in ES5
Closes #2534
This commit is contained in:
parent
4f581671dc
commit
c3ae34f066
|
@ -6,6 +6,8 @@
|
|||
export {
|
||||
Component as ComponentAnnotation,
|
||||
Directive as DirectiveAnnotation,
|
||||
ComponentArgs,
|
||||
DirectiveArgs,
|
||||
onDestroy,
|
||||
onChange,
|
||||
onCheck,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
export {
|
||||
View as ViewAnnotation,
|
||||
} from '../annotations_impl/view';
|
||||
export {View as ViewAnnotation, ViewArgs} from '../annotations_impl/view';
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
library angular2.test.core.annotations.decorators_dart_spec;
|
||||
|
||||
main() {
|
||||
// not relavant for dart.
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -21,4 +21,4 @@ export function paramDecorator(value) {
|
|||
}
|
||||
|
||||
export var ClassDecorator = makeDecorator(ClassDecoratorImpl);
|
||||
export var ParamDecorator = makeParamDecorator(ParamDecoratorImpl);
|
||||
export var ParamDecorator = makeParamDecorator(ParamDecoratorImpl);
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
library angular2.test.util.decorators_dart_spec;
|
||||
|
||||
main() {
|
||||
// not relavant for dart.
|
||||
}
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue