feat(Compiler): Multiple template per component

fixes #596

- TemplateConfig becomes Template
- introduce a TemplateResolver to pick the cmp template,
- @Component and @Template are disociated
This commit is contained in:
Victor Berchet
2015-02-12 14:44:59 +01:00
parent 52b062621d
commit e6c8bde808
35 changed files with 953 additions and 724 deletions

View File

@ -1,6 +1,5 @@
import {ABSTRACT, CONST, normalizeBlank, isPresent} from 'angular2/src/facade/lang';
import {ListWrapper, List} from 'angular2/src/facade/collection';
import {TemplateConfig} from './template_config';
@ABSTRACT()
export class Directive {
@ -38,7 +37,6 @@ export class Directive {
export class Component extends Directive {
//TODO: vsavkin: uncomment it once the issue with defining fields in a sublass works
template:any; //TemplateConfig;
lightDomServices:any; //List;
shadowDomServices:any; //List;
componentServices:any; //List;
@ -48,7 +46,6 @@ export class Component extends Directive {
constructor({
selector,
bind,
template,
lightDomServices,
shadowDomServices,
componentServices,
@ -57,7 +54,6 @@ export class Component extends Directive {
}:{
selector:String,
bind:Object,
template:TemplateConfig,
lightDomServices:List,
shadowDomServices:List,
componentServices:List,
@ -73,7 +69,6 @@ export class Component extends Directive {
lifecycle: lifecycle
});
this.template = template;
this.lightDomServices = lightDomServices;
this.shadowDomServices = shadowDomServices;
this.componentServices = componentServices;

View File

@ -1,25 +1,31 @@
import {ABSTRACT, CONST, Type} from 'angular2/src/facade/lang';
import {List} from 'angular2/src/facade/collection';
export class TemplateConfig {
export class Template {
url:any; //string;
inline:any; //string;
directives:any; //List<Type>;
formatters:any; //List<Type>;
source:any;//List<TemplateConfig>;
source:any;//List<Template>;
locale:any; //string
device:any; //string
@CONST()
constructor({
url,
inline,
directives,
formatters,
source
source,
locale,
device
}: {
url: string,
inline: string,
directives: List<Type>,
formatters: List<Type>,
source: List<TemplateConfig>
source: List<Template>,
locale: string,
device: string
})
{
this.url = url;
@ -27,5 +33,7 @@ export class TemplateConfig {
this.directives = directives;
this.formatters = formatters;
this.source = source;
this.locale = locale;
this.device = device;
}
}

View File

@ -6,6 +6,7 @@ import {ProtoView} from './compiler/view';
import {Reflector, reflector} from 'angular2/src/reflection/reflection';
import {Parser, Lexer, ChangeDetection, dynamicChangeDetection, jitChangeDetection} from 'angular2/change_detection';
import {TemplateLoader} from './compiler/template_loader';
import {TemplateResolver} from './compiler/template_resolver';
import {DirectiveMetadataReader} from './compiler/directive_metadata_reader';
import {DirectiveMetadata} from './compiler/directive_metadata';
import {List, ListWrapper} from 'angular2/src/facade/collection';
@ -28,6 +29,7 @@ var _rootBindings = [
Compiler,
CompilerCache,
TemplateLoader,
TemplateResolver,
DirectiveMetadataReader,
Parser,
Lexer,
@ -62,7 +64,7 @@ function _injectorBindings(appComponentType): List<Binding> {
bind(appViewToken).toAsyncFactory((changeDetection, compiler, injector, appElement,
appComponentAnnotatedType, strategy, eventManager) => {
return compiler.compile(appComponentAnnotatedType.type, null).then(
return compiler.compile(appComponentAnnotatedType.type).then(
(protoView) => {
var appProtoView = ProtoView.createRootProtoView(protoView, appElement,
appComponentAnnotatedType, changeDetection.createProtoChangeDetector('root'),

View File

@ -11,8 +11,10 @@ import {CompilePipeline} from './pipeline/compile_pipeline';
import {CompileElement} from './pipeline/compile_element';
import {createDefaultSteps} from './pipeline/default_steps';
import {TemplateLoader} from './template_loader';
import {TemplateResolver} from './template_resolver';
import {DirectiveMetadata} from './directive_metadata';
import {Component} from '../annotations/annotations';
import {Template} from '../annotations/template';
import {Content} from './shadow_dom_emulation/content_tag';
import {ShadowDomStrategy} from './shadow_dom_strategy';
import {CompileStep} from './pipeline/compile_step';
@ -55,13 +57,15 @@ export class Compiler {
_compiling:Map<Type, Promise>;
_shadowDomStrategy: ShadowDomStrategy;
_shadowDomDirectives: List<DirectiveMetadata>;
_templateResolver: TemplateResolver;
constructor(changeDetection:ChangeDetection,
templateLoader:TemplateLoader,
reader: DirectiveMetadataReader,
parser:Parser,
cache:CompilerCache,
shadowDomStrategy: ShadowDomStrategy) {
shadowDomStrategy: ShadowDomStrategy,
templateResolver: TemplateResolver) {
this._changeDetection = changeDetection;
this._reader = reader;
this._parser = parser;
@ -74,32 +78,38 @@ export class Compiler {
for (var i = 0; i < types.length; i++) {
ListWrapper.push(this._shadowDomDirectives, reader.read(types[i]));
}
this._templateResolver = templateResolver;
}
createSteps(component:DirectiveMetadata):List<CompileStep> {
var directives = []
var cmpDirectives = ListWrapper.map(component.componentDirectives, (d) => this._reader.read(d));
directives = ListWrapper.concat(directives, cmpDirectives);
directives = ListWrapper.concat(directives, this._shadowDomDirectives);
return createDefaultSteps(this._changeDetection, this._parser, component, directives,
createSteps(component:Type, template: Template):List<CompileStep> {
// Merge directive metadata (from the template and from the shadow dom strategy)
var dirMetadata = [];
var tplMetadata = ListWrapper.map(this._flattenDirectives(template),
(d) => this._reader.read(d));
dirMetadata = ListWrapper.concat(dirMetadata, tplMetadata);
dirMetadata = ListWrapper.concat(dirMetadata, this._shadowDomDirectives);
var cmpMetadata = this._reader.read(component);
return createDefaultSteps(this._changeDetection, this._parser, cmpMetadata, dirMetadata,
this._shadowDomStrategy);
}
compile(component:Type, templateRoot:Element = null):Promise<ProtoView> {
var protoView = this._compile(this._reader.read(component), templateRoot);
compile(component: Type):Promise<ProtoView> {
var protoView = this._compile(component);
return PromiseWrapper.isPromise(protoView) ? protoView : PromiseWrapper.resolve(protoView);
}
// TODO(vicb): union type return ProtoView or Promise<ProtoView>
_compile(cmpMetadata: DirectiveMetadata, templateRoot:Element = null) {
var protoView = this._compilerCache.get(cmpMetadata.type);
_compile(component: Type) {
var protoView = this._compilerCache.get(component);
if (isPresent(protoView)) {
// The component has already been compiled into a ProtoView,
// returns a resolved Promise.
return protoView;
}
var pvPromise = MapWrapper.get(this._compiling, cmpMetadata.type);
var pvPromise = MapWrapper.get(this._compiling, component);
if (isPresent(pvPromise)) {
// The component is already being compiled, attach to the existing Promise
// instead of re-compiling the component.
@ -107,30 +117,32 @@ export class Compiler {
return pvPromise;
}
var template = isBlank(templateRoot) ? this._templateLoader.load(cmpMetadata) : templateRoot;
var template = this._templateResolver.resolve(component);
if (PromiseWrapper.isPromise(template)) {
pvPromise = PromiseWrapper.then(template,
(el) => this._compileTemplate(el, cmpMetadata),
(_) => { throw new BaseException(`Failed to load the template for ${stringify(cmpMetadata.type)}`); }
var tplElement = this._templateLoader.load(template);
if (PromiseWrapper.isPromise(tplElement)) {
pvPromise = PromiseWrapper.then(tplElement,
(el) => this._compileTemplate(template, el, component),
(_) => { throw new BaseException(`Failed to load the template for ${stringify(component)}`); }
);
MapWrapper.set(this._compiling, cmpMetadata.type, pvPromise);
MapWrapper.set(this._compiling, component, pvPromise);
return pvPromise;
}
return this._compileTemplate(template, cmpMetadata);
return this._compileTemplate(template, tplElement, component);
}
// TODO(vicb): union type return ProtoView or Promise<ProtoView>
_compileTemplate(template: Element, cmpMetadata) {
var pipeline = new CompilePipeline(this.createSteps(cmpMetadata));
var compileElements = pipeline.process(template);
_compileTemplate(template: Template, tplElement: Element, component: Type) {
var pipeline = new CompilePipeline(this.createSteps(component, template));
var compileElements = pipeline.process(tplElement);
var protoView = compileElements[0].inheritedProtoView;
// Populate the cache before compiling the nested components,
// so that components can reference themselves in their template.
this._compilerCache.set(cmpMetadata.type, protoView);
MapWrapper.delete(this._compiling, cmpMetadata.type);
this._compilerCache.set(component, protoView);
MapWrapper.delete(this._compiling, component);
// Compile all the components from the template
var nestedPVPromises = [];
@ -146,7 +158,7 @@ export class Compiler {
// The promise will resolved after nested ProtoViews are compiled.
return PromiseWrapper.then(PromiseWrapper.all(nestedPVPromises),
(_) => protoView,
(e) => { throw new BaseException(`${e.message} -> Failed to compile ${stringify(cmpMetadata.type)}`); }
(e) => { throw new BaseException(`${e.message} -> Failed to compile ${stringify(component)}`); }
);
}
@ -154,9 +166,8 @@ export class Compiler {
return protoView;
}
_compileNestedProtoView(ce: CompileElement, promises: List<Promise>)
{
var protoView = this._compile(ce.componentDirective);
_compileNestedProtoView(ce: CompileElement, promises: List<Promise>) {
var protoView = this._compile(ce.componentDirective.type);
if (PromiseWrapper.isPromise(protoView)) {
ListWrapper.push(promises, protoView);
@ -167,4 +178,27 @@ export class Compiler {
ce.inheritedElementBinder.nestedProtoView = protoView;
}
}
_flattenDirectives(template: Template):List<Type> {
if (isBlank(template.directives)) return [];
var directives = [];
this._flattenList(template.directives, directives);
return directives;
}
_flattenList(tree:List<any>, out:List<Type>) {
for (var i = 0; i < tree.length; i++) {
var item = tree[i];
if (ListWrapper.isList(item)) {
this._flattenList(item, out);
} else {
ListWrapper.push(out, item);
}
}
}
}

View File

@ -1,7 +1,5 @@
import {Type} from 'angular2/src/facade/lang';
import {Directive} from 'angular2/src/core/annotations/annotations'
import {List} from 'angular2/src/facade/collection'
import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy';
/**
* Combination of a type with the Directive annotation
@ -9,13 +7,9 @@ import {ShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'
export class DirectiveMetadata {
type:Type;
annotation:Directive;
componentDirectives:List<Type>;
constructor(type:Type,
annotation:Directive,
componentDirectives:List<Type>) {
constructor(type:Type, annotation:Directive) {
this.annotation = annotation;
this.type = type;
this.componentDirectives = componentDirectives;
}
}

View File

@ -1,9 +1,7 @@
import {Type, isPresent, BaseException, stringify} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {Directive, Component} from '../annotations/annotations';
import {Directive} from '../annotations/annotations';
import {DirectiveMetadata} from './directive_metadata';
import {reflector} from 'angular2/src/reflection/reflection';
import {ShadowDom, ShadowDomStrategy, ShadowDomNative} from './shadow_dom_strategy';
export class DirectiveMetadataReader {
read(type:Type):DirectiveMetadata {
@ -12,39 +10,12 @@ export class DirectiveMetadataReader {
for (var i=0; i<annotations.length; i++) {
var annotation = annotations[i];
if (annotation instanceof Component) {
return new DirectiveMetadata(
type,
annotation,
this.componentDirectivesMetadata(annotation)
);
}
if (annotation instanceof Directive) {
return new DirectiveMetadata(type, annotation, null);
return new DirectiveMetadata(type, annotation);
}
}
}
throw new BaseException(`No Directive annotation found on ${stringify(type)}`);
}
componentDirectivesMetadata(annotation:Component):List<Type> {
var template = annotation.template;
var result:List<Type> = ListWrapper.create();
if (isPresent(template) && isPresent(template.directives)) {
this._buildList(result, template.directives);
}
return result;
}
_buildList(out:List<Type>, tree:List<any>) {
for (var i = 0; i < tree.length; i++) {
var item = tree[i];
if (ListWrapper.isList(item)) {
this._buildList(out, item);
} else {
ListWrapper.push(out, item);
}
}
}
}

View File

@ -1,15 +1,12 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isBlank, isPresent, BaseException, stringify} from 'angular2/src/facade/lang';
import {TemplateElement, DOM, Element} from 'angular2/src/facade/dom';
import {DOM, Element} from 'angular2/src/facade/dom';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {TemplateConfig} from 'angular2/src/core/annotations/template_config';
import {Component} from 'angular2/src/core/annotations/annotations';
import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata';
import {XHR} from './xhr/xhr';
import {Template} from 'angular2/src/core/annotations/template';
/**
* Strategy to load component templates.
*/
@ -23,16 +20,13 @@ export class TemplateLoader {
}
// TODO(vicb): union type: return an Element or a Promise<Element>
load(cmpMetadata: DirectiveMetadata) {
var annotation:Component = cmpMetadata.annotation;
var tplConfig:TemplateConfig = annotation.template;
if (isPresent(tplConfig.inline)) {
return DOM.createTemplate(tplConfig.inline);
load(template: Template) {
if (isPresent(template.inline)) {
return DOM.createTemplate(template.inline);
}
if (isPresent(tplConfig.url)) {
var url = tplConfig.url;
if (isPresent(template.url)) {
var url = template.url;
var promise = StringMapWrapper.get(this._cache, url);
if (isBlank(promise)) {
@ -46,6 +40,6 @@ export class TemplateLoader {
return promise;
}
throw new BaseException(`No template configured for component ${stringify(cmpMetadata.type)}`);
throw new BaseException(`Templates should have either their url or inline property set`);
}
}

View File

@ -0,0 +1,38 @@
import {Template} from 'angular2/src/core/annotations/template';
import {Type, stringify, isBlank, BaseException} from 'angular2/src/facade/lang';
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
import {reflector} from 'angular2/src/reflection/reflection';
export class TemplateResolver {
_cache: Map;
constructor() {
this._cache = MapWrapper.create();
}
resolve(component: Type): Template {
var template = MapWrapper.get(this._cache, component);
if (isBlank(template)) {
template = this._resolve(component);
MapWrapper.set(this._cache, component, template);
}
return template;
}
_resolve(component: Type) {
var annotations = reflector.annotations(component);
for (var i = 0; i < annotations.length; i++) {
var annotation = annotations[i];
if (annotation instanceof Template) {
return annotation;
}
}
throw new BaseException(`No template found for ${stringify(component)}`);
}
}

View File

@ -1,4 +1,4 @@
import {TemplateConfig, Component, Decorator, NgElement, Ancestor, onChange} from 'angular2/core';
import {Template, Component, Decorator, NgElement, Ancestor, onChange} from 'angular2/core';
import {DOM} from 'angular2/src/facade/dom';
import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang';
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
@ -164,11 +164,9 @@ export class ControlGroupDirective extends ControlGroupDirectiveBase {
selector: '[new-control-group]',
bind: {
'new-control-group' : 'initData'
},
template: new TemplateConfig({
inline: '<content>'
})
}
})
@Template({inline: '<content>'})
export class NewControlGroupDirective extends ControlGroupDirectiveBase {
_initData:any;
_controlGroup:ControlGroup;