diff --git a/modules/angular2/src/core/linker/view_resolver.ts b/modules/angular2/src/core/linker/view_resolver.ts index 237e37358b..22ce898c1c 100644 --- a/modules/angular2/src/core/linker/view_resolver.ts +++ b/modules/angular2/src/core/linker/view_resolver.ts @@ -1,7 +1,8 @@ import {Injectable} from 'angular2/src/core/di'; import {ViewMetadata} from '../metadata/view'; +import {ComponentMetadata} from '../metadata/directives'; -import {Type, stringify, isBlank} from 'angular2/src/core/facade/lang'; +import {Type, stringify, isBlank, isPresent} from 'angular2/src/core/facade/lang'; import {BaseException} from 'angular2/src/core/facade/exceptions'; import {Map, MapWrapper, ListWrapper} from 'angular2/src/core/facade/collection'; @@ -24,13 +25,70 @@ export class ViewResolver { } _resolve(component: Type): ViewMetadata { - var annotations = reflector.annotations(component); - for (var i = 0; i < annotations.length; i++) { - var annotation = annotations[i]; - if (annotation instanceof ViewMetadata) { - return annotation; + var compMeta: ComponentMetadata; + var viewMeta: ViewMetadata; + + reflector.annotations(component).forEach(m => { + if (m instanceof ViewMetadata) { + viewMeta = m; + } + if (m instanceof ComponentMetadata) { + compMeta = m; + } + }); + + if (isPresent(compMeta)) { + if (isBlank(compMeta.template) && isBlank(compMeta.templateUrl) && isBlank(viewMeta)) { + throw new BaseException( + `Component '${stringify(component)}' must have either 'template', 'templateUrl', or '@View' set.`); + + } else if (isPresent(compMeta.template) && isPresent(viewMeta)) { + this._throwMixingViewAndComponent("template", component); + + } else if (isPresent(compMeta.templateUrl) && isPresent(viewMeta)) { + this._throwMixingViewAndComponent("templateUrl", component); + + } else if (isPresent(compMeta.directives) && isPresent(viewMeta)) { + this._throwMixingViewAndComponent("directives", component); + + } else if (isPresent(compMeta.pipes) && isPresent(viewMeta)) { + this._throwMixingViewAndComponent("pipes", component); + + } else if (isPresent(compMeta.encapsulation) && isPresent(viewMeta)) { + this._throwMixingViewAndComponent("encapsulation", component); + + } else if (isPresent(compMeta.styles) && isPresent(viewMeta)) { + this._throwMixingViewAndComponent("styles", component); + + } else if (isPresent(compMeta.styleUrls) && isPresent(viewMeta)) { + this._throwMixingViewAndComponent("styleUrls", component); + + } else if (isPresent(viewMeta)) { + return viewMeta; + + } else { + return new ViewMetadata({ + templateUrl: compMeta.templateUrl, + template: compMeta.template, + directives: compMeta.directives, + pipes: compMeta.pipes, + encapsulation: compMeta.encapsulation, + styles: compMeta.styles, + styleUrls: compMeta.styleUrls + }); + } + } else { + if (isBlank(viewMeta)) { + throw new BaseException(`No View decorator found on component '${stringify(component)}'`); + } else { + return viewMeta; } } - throw new BaseException(`No View annotation found on component ${stringify(component)}`); + return null; + } + + _throwMixingViewAndComponent(propertyName: string, component: Type): void { + throw new BaseException( + `Component '${stringify(component)}' cannot have both '${propertyName}' and '@View' set at the same time"`); } } diff --git a/modules/angular2/src/core/metadata.dart b/modules/angular2/src/core/metadata.dart index 600267e4e2..f8d17d96b5 100644 --- a/modules/angular2/src/core/metadata.dart +++ b/modules/angular2/src/core/metadata.dart @@ -45,7 +45,11 @@ class Component extends ComponentMetadata { Map host, List bindings, String exportAs, String moduleId, Map queries, - List viewBindings, ChangeDetectionStrategy changeDetection}) + List viewBindings, ChangeDetectionStrategy changeDetection, + String templateUrl, String template, dynamic directives, + dynamic pipes, ViewEncapsulation encapsulation, List styles, + List styleUrls + }) : super( selector: selector, inputs: inputs, @@ -58,7 +62,15 @@ class Component extends ComponentMetadata { moduleId: moduleId, viewBindings: viewBindings, queries: queries, - changeDetection: changeDetection); + changeDetection: changeDetection, + templateUrl: templateUrl, + template: template, + directives: directives, + pipes: pipes, + encapsulation: encapsulation, + styles: styles, + styleUrls: styleUrls + ); } /** diff --git a/modules/angular2/src/core/metadata.ts b/modules/angular2/src/core/metadata.ts index c198d6c701..c4d89c1e36 100644 --- a/modules/angular2/src/core/metadata.ts +++ b/modules/angular2/src/core/metadata.ts @@ -228,6 +228,13 @@ export interface ComponentFactory { queries?: {[key: string]: any}, viewBindings?: any[], changeDetection?: ChangeDetectionStrategy, + templateUrl?: string, + template?: string, + styleUrls?: string[], + styles?: string[], + directives?: Array, + pipes?: Array, + encapsulation?: ViewEncapsulation }): ComponentDecorator; new (obj: { selector?: string, @@ -242,6 +249,13 @@ export interface ComponentFactory { queries?: {[key: string]: any}, viewBindings?: any[], changeDetection?: ChangeDetectionStrategy, + templateUrl?: string, + template?: string, + styleUrls?: string[], + styles?: string[], + directives?: Array, + pipes?: Array, + encapsulation?: ViewEncapsulation }): ComponentMetadata; } diff --git a/modules/angular2/src/core/metadata/directives.ts b/modules/angular2/src/core/metadata/directives.ts index 4447e914f0..6490ae2121 100644 --- a/modules/angular2/src/core/metadata/directives.ts +++ b/modules/angular2/src/core/metadata/directives.ts @@ -1,6 +1,7 @@ import {isPresent, CONST, CONST_EXPR, Type} from 'angular2/src/core/facade/lang'; import {InjectableMetadata} from 'angular2/src/core/di/metadata'; import {ChangeDetectionStrategy} from 'angular2/src/core/change_detection'; +import {ViewEncapsulation} from 'angular2/src/core/metadata/view'; /** * Directives allow you to attach behavior to elements in the DOM. @@ -876,8 +877,23 @@ export class ComponentMetadata extends DirectiveMetadata { */ viewBindings: any[]; + templateUrl: string; + + template: string; + + styleUrls: string[]; + + styles: string[]; + + directives: Array; + + pipes: Array; + + encapsulation: ViewEncapsulation; + constructor({selector, inputs, outputs, properties, events, host, exportAs, moduleId, bindings, - viewBindings, changeDetection = ChangeDetectionStrategy.Default, queries}: { + viewBindings, changeDetection = ChangeDetectionStrategy.Default, queries, + templateUrl, template, styleUrls, styles, directives, pipes, encapsulation}: { selector?: string, inputs?: string[], outputs?: string[], @@ -890,6 +906,13 @@ export class ComponentMetadata extends DirectiveMetadata { viewBindings?: any[], queries?: {[key: string]: any}, changeDetection?: ChangeDetectionStrategy, + templateUrl?: string, + template?: string, + styleUrls?: string[], + styles?: string[], + directives?: Array, + pipes?: Array, + encapsulation?: ViewEncapsulation } = {}) { super({ selector: selector, @@ -906,6 +929,14 @@ export class ComponentMetadata extends DirectiveMetadata { this.changeDetection = changeDetection; this.viewBindings = viewBindings; + + this.templateUrl = templateUrl; + this.template = template; + this.styleUrls = styleUrls; + this.styles = styles; + this.directives = directives; + this.pipes = pipes; + this.encapsulation = encapsulation; } } diff --git a/modules/angular2/test/core/linker/integration_spec.ts b/modules/angular2/test/core/linker/integration_spec.ts index 5b400ae7f3..bbeee80293 100644 --- a/modules/angular2/test/core/linker/integration_spec.ts +++ b/modules/angular2/test/core/linker/integration_spec.ts @@ -1231,8 +1231,8 @@ export function main() { try { tcb.createAsync(ComponentWithoutView); } catch (e) { - expect(e.message).toEqual( - `No View annotation found on component ${stringify(ComponentWithoutView)}`); + expect(e.message) + .toContain(`must have either 'template', 'templateUrl', or '@View' set.`); return null; } })); @@ -1696,6 +1696,21 @@ export function main() { }); })); } + + it('should support defining views in the component decorator', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView(MyComp, new ViewMetadata({ + template: '', + directives: [ComponentWithTempalte] + })) + .createAsync(MyComp) + .then((rootTC) => { + rootTC.detectChanges(); + var native = rootTC.debugElement.componentViewChildren[0].nativeElement; + expect(native).toHaveText("No View Decorator: 123"); + async.done(); + }); + })); }); }); } @@ -2251,6 +2266,14 @@ class DirectiveThrowingAnError { constructor() { throw new BaseException("BOOM"); } } +@Component({ + selector: 'component-with-template', + directives: [NgFor], template: `No View Decorator:
{{item}}
` +}) +class ComponentWithTempalte { + items = [1, 2, 3]; +} + @Directive({selector: 'with-prop-decorators'}) class DirectiveWithPropDecorators { target; diff --git a/modules/angular2/test/core/linker/view_resolver_spec.ts b/modules/angular2/test/core/linker/view_resolver_spec.ts new file mode 100644 index 0000000000..77a9741f9d --- /dev/null +++ b/modules/angular2/test/core/linker/view_resolver_spec.ts @@ -0,0 +1,100 @@ +import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib'; +import {ViewResolver} from 'angular2/src/core/linker/view_resolver'; +import {Component, View, ViewMetadata} from 'angular2/src/core/metadata'; + +class SomeDir {} +class SomePipe {} + +@Component({selector: 'sample'}) +@View( + {template: "some template", directives: [SomeDir], pipes: [SomePipe], styles: ["some styles"]}) +class ComponentWithView { +} + +@Component({ + selector: 'sample', + template: "some template", + directives: [SomeDir], + pipes: [SomePipe], + styles: ["some styles"] +}) +class ComponentWithTemplate { +} + +@Component({selector: 'sample', template: "some template"}) +@View({template: "some template"}) +class ComponentWithViewTemplate { +} + +@Component({selector: 'sample'}) +class ComponentWithoutView { +} + +@Component({selector: 'sample', templateUrl: "some template url"}) +@View({template: "some template"}) +class ComponentWithViewTemplateUrl { +} + +@View({template: "some template"}) +class ClassWithView { +} + +class SimpleClass {} + +export function main() { + describe("ViewResolver", () => { + var resolver; + + beforeEach(() => { resolver = new ViewResolver(); }); + + it('should read out the View metadata', () => { + var viewMetadata = resolver.resolve(ComponentWithView); + expect(viewMetadata) + .toEqual(new View({ + template: "some template", + directives: [SomeDir], + pipes: [SomePipe], + styles: ["some styles"] + })); + }); + + it('should read out the View metadata from the Component metadata', () => { + var viewMetadata = resolver.resolve(ComponentWithTemplate); + expect(viewMetadata) + .toEqual(new ViewMetadata({ + template: "some template", + directives: [SomeDir], + pipes: [SomePipe], + styles: ["some styles"] + })); + }); + + it('should read out the View metadata from a simple class', () => { + var viewMetadata = resolver.resolve(ClassWithView); + expect(viewMetadata).toEqual(new View({template: "some template"})); + }); + + it('should throw when Component.template is specified together with the View metadata', () => { + expect(() => resolver.resolve(ComponentWithViewTemplate)) + .toThrowErrorWith( + "Component 'ComponentWithViewTemplate' cannot have both 'template' and '@View' set at the same time"); + }); + + it('should throw when Component.template is specified together with the View metadata', () => { + expect(() => resolver.resolve(ComponentWithViewTemplateUrl)) + .toThrowErrorWith( + "Component 'ComponentWithViewTemplateUrl' cannot have both 'templateUrl' and '@View' set at the same time"); + }); + + it('should throw when Component has no View decorator and no template is set', () => { + expect(() => resolver.resolve(ComponentWithoutView)) + .toThrowErrorWith( + "Component 'ComponentWithoutView' must have either 'template', 'templateUrl', or '@View' set"); + }); + + it('should throw when simple class has no View decorator and no template is set', () => { + expect(() => resolver.resolve(SimpleClass)) + .toThrowErrorWith("No View decorator found on component 'SimpleClass'"); + }); + }); +} diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 64f3ae3b02..dcc7f88b8f 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -166,6 +166,13 @@ var NG_API = [ 'Component.queries', 'Component.selector', 'Component.viewBindings', + 'Component.directives', + 'Component.encapsulation', + 'Component.pipes', + 'Component.styleUrls', + 'Component.styles', + 'Component.template', + 'Component.templateUrl', 'ComponentMetadata', 'ComponentMetadata.bindings', 'ComponentMetadata.changeDetection', @@ -179,6 +186,13 @@ var NG_API = [ 'ComponentMetadata.queries', 'ComponentMetadata.selector', 'ComponentMetadata.viewBindings', + 'ComponentMetadata.directives', + 'ComponentMetadata.encapsulation', + 'ComponentMetadata.pipes', + 'ComponentMetadata.styleUrls', + 'ComponentMetadata.styles', + 'ComponentMetadata.template', + 'ComponentMetadata.templateUrl', 'ComponentRef', 'ComponentRef.componentType', 'ComponentRef.componentType=',