feat(core): add syntax sugar to make @View optional

This commit is contained in:
vsavkin 2015-10-06 17:03:37 -07:00 committed by Victor Savkin
parent f7b75330e0
commit bd31b01690
7 changed files with 264 additions and 12 deletions

View File

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

View File

@ -45,7 +45,11 @@ class Component extends ComponentMetadata {
Map<String, String> host,
List bindings, String exportAs, String moduleId,
Map<String, dynamic> queries,
List viewBindings, ChangeDetectionStrategy changeDetection})
List viewBindings, ChangeDetectionStrategy changeDetection,
String templateUrl, String template, dynamic directives,
dynamic pipes, ViewEncapsulation encapsulation, List<String> styles,
List<String> 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
);
}
/**

View File

@ -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<Type | any[]>,
pipes?: Array<Type | any[]>,
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<Type | any[]>,
pipes?: Array<Type | any[]>,
encapsulation?: ViewEncapsulation
}): ComponentMetadata;
}

View File

@ -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<Type | any[]>;
pipes: Array<Type | any[]>;
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<Type | any[]>,
pipes?: Array<Type | any[]>,
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;
}
}

View File

@ -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: '<component-with-template></component-with-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: <div *ng-for="#item of items">{{item}}</div>`
})
class ComponentWithTempalte {
items = [1, 2, 3];
}
@Directive({selector: 'with-prop-decorators'})
class DirectiveWithPropDecorators {
target;

View File

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

View File

@ -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=',