From 0246b2a2cb1bc753b01a659404a46acd98a35415 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 11 Sep 2015 13:37:05 -0700 Subject: [PATCH] feat(compiler): support creating template commands Closes #4142 --- modules/angular2/src/compiler/api.ts | 2 +- .../src/compiler/change_definition_factory.ts | 101 ++-- .../angular2/src/compiler/command_compiler.ts | 241 +++++++++ .../angular2/src/compiler/style_compiler.ts | 22 +- modules/angular2/src/compiler/template_ast.ts | 28 +- .../angular2/src/compiler/template_parser.ts | 104 ++-- modules/angular2/src/compiler/util.ts | 29 +- .../src/core/compiler/template_commands.ts | 141 +++++ modules/angular2/src/core/render/api.ts | 41 +- .../test/compiler/command_compiler_spec.ts | 489 ++++++++++++++++++ .../test/compiler/style_compiler_spec.ts | 8 +- .../test/compiler/template_parser_spec.ts | 242 +++++++-- modules/angular2/test/compiler/util_spec.ts | 42 ++ 13 files changed, 1324 insertions(+), 166 deletions(-) create mode 100644 modules/angular2/src/compiler/command_compiler.ts create mode 100644 modules/angular2/src/core/compiler/template_commands.ts create mode 100644 modules/angular2/test/compiler/command_compiler_spec.ts create mode 100644 modules/angular2/test/compiler/util_spec.ts diff --git a/modules/angular2/src/compiler/api.ts b/modules/angular2/src/compiler/api.ts index bcbb8a07f9..0dd0757700 100644 --- a/modules/angular2/src/compiler/api.ts +++ b/modules/angular2/src/compiler/api.ts @@ -134,7 +134,7 @@ export class TemplateMetadata { this.ngContentSelectors = ngContentSelectors; } - static fromJson(data: StringMap):TemplateMetadata { + static fromJson(data: StringMap): TemplateMetadata { return new TemplateMetadata({ encapsulation: isPresent(data['encapsulation']) ? viewEncapsulationFromJson(data['encapsulation']) : diff --git a/modules/angular2/src/compiler/change_definition_factory.ts b/modules/angular2/src/compiler/change_definition_factory.ts index 0eab35fcad..6ff6fab91b 100644 --- a/modules/angular2/src/compiler/change_definition_factory.ts +++ b/modules/angular2/src/compiler/change_definition_factory.ts @@ -36,47 +36,43 @@ import { export function createChangeDetectorDefinitions( componentType: TypeMetadata, componentStrategy: ChangeDetectionStrategy, genConfig: ChangeDetectorGenConfig, parsedTemplate: TemplateAst[]): ChangeDetectorDefinition[] { - var visitor = new ProtoViewVisitor(componentStrategy); + var pvVisitors = []; + var visitor = new ProtoViewVisitor(null, pvVisitors, componentStrategy); templateVisitAll(visitor, parsedTemplate); - return createChangeDefinitions(visitor.allProtoViews, componentType, genConfig); + return createChangeDefinitions(pvVisitors, componentType, genConfig); } class ProtoViewVisitor implements TemplateAstVisitor { - viewCount: number = 0; - protoViewStack: ProtoViewVisitorData[] = []; - allProtoViews: ProtoViewVisitorData[] = []; + viewIndex: number; + boundTextCount: number = 0; + boundElementCount: number = 0; + variableNames: string[] = []; + bindingRecords: BindingRecord[] = []; + eventRecords: BindingRecord[] = []; + directiveRecords: DirectiveRecord[] = []; - constructor(componentStrategy: ChangeDetectionStrategy) { - this._beginProtoView(new ProtoViewVisitorData(null, componentStrategy, this.viewCount++)); - } - - private _beginProtoView(data: ProtoViewVisitorData) { - this.protoViewStack.push(data); - this.allProtoViews.push(data); - } - - get currentProtoView(): ProtoViewVisitorData { - return this.protoViewStack[this.protoViewStack.length - 1]; + constructor(public parent: ProtoViewVisitor, public allVisitors: ProtoViewVisitor[], + public strategy: ChangeDetectionStrategy) { + this.viewIndex = allVisitors.length; + allVisitors.push(this); } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { - this.currentProtoView.boundElementCount++; + this.boundElementCount++; templateVisitAll(this, ast.directives); - this.viewCount++; - this._beginProtoView(new ProtoViewVisitorData( - this.currentProtoView, ChangeDetectionStrategy.Default, this.viewCount - 1)); + var childVisitor = + new ProtoViewVisitor(this, this.allVisitors, ChangeDetectionStrategy.Default); // Attention: variables present on an embedded template count towards // the embedded template and not the template anchor! - templateVisitAll(this, ast.vars); - templateVisitAll(this, ast.children); - this.protoViewStack.pop(); + templateVisitAll(childVisitor, ast.vars); + templateVisitAll(childVisitor, ast.children); return null; } visitElement(ast: ElementAst, context: any): any { if (ast.isBound()) { - this.currentProtoView.boundElementCount++; + this.boundElementCount++; } templateVisitAll(this, ast.properties, null); templateVisitAll(this, ast.events); @@ -91,7 +87,7 @@ class ProtoViewVisitor implements TemplateAstVisitor { visitNgContent(ast: NgContentAst, context: any): any { return null; } visitVariable(ast: VariableAst, context: any): any { - this.currentProtoView.variableNames.push(ast.name); + this.variableNames.push(ast.name); return null; } @@ -99,14 +95,13 @@ class ProtoViewVisitor implements TemplateAstVisitor { var bindingRecord = isPresent(directiveRecord) ? BindingRecord.createForHostEvent(ast.handler, ast.name, directiveRecord) : - BindingRecord.createForEvent(ast.handler, ast.name, - this.currentProtoView.boundElementCount - 1); - this.currentProtoView.eventRecords.push(bindingRecord); + BindingRecord.createForEvent(ast.handler, ast.name, this.boundElementCount - 1); + this.eventRecords.push(bindingRecord); return null; } visitElementProperty(ast: BoundElementPropertyAst, directiveRecord: DirectiveRecord): any { - var boundElementIndex = this.currentProtoView.boundElementCount - 1; + var boundElementIndex = this.boundElementCount - 1; var dirIndex = isPresent(directiveRecord) ? directiveRecord.directiveIndex : null; var bindingRecord; if (ast.type === PropertyBindingType.Property) { @@ -130,20 +125,18 @@ class ProtoViewVisitor implements TemplateAstVisitor { BindingRecord.createForHostStyle(dirIndex, ast.value, ast.name, ast.unit) : BindingRecord.createForElementStyle(ast.value, boundElementIndex, ast.name, ast.unit); } - this.currentProtoView.bindingRecords.push(bindingRecord); + this.bindingRecords.push(bindingRecord); return null; } visitAttr(ast: AttrAst, context: any): any { return null; } visitBoundText(ast: BoundTextAst, context: any): any { - var boundTextIndex = this.currentProtoView.boundTextCount++; - this.currentProtoView.bindingRecords.push( - BindingRecord.createForTextNode(ast.value, boundTextIndex)); + var boundTextIndex = this.boundTextCount++; + this.bindingRecords.push(BindingRecord.createForTextNode(ast.value, boundTextIndex)); return null; } visitText(ast: TextAst, context: any): any { return null; } visitDirective(ast: DirectiveAst, directiveIndexAsNumber: number): any { - var directiveIndex = - new DirectiveIndex(this.currentProtoView.boundElementCount - 1, directiveIndexAsNumber); + var directiveIndex = new DirectiveIndex(this.boundElementCount - 1, directiveIndexAsNumber); var directiveMetadata = ast.directive; var changeDetectionMeta = directiveMetadata.changeDetection; var directiveRecord = new DirectiveRecord({ @@ -157,10 +150,10 @@ class ProtoViewVisitor implements TemplateAstVisitor { callOnInit: changeDetectionMeta.callOnInit, changeDetection: changeDetectionMeta.changeDetection }); - this.currentProtoView.directiveRecords.push(directiveRecord); + this.directiveRecords.push(directiveRecord); templateVisitAll(this, ast.properties, directiveRecord); - var bindingRecords = this.currentProtoView.bindingRecords; + var bindingRecords = this.bindingRecords; if (directiveRecord.callOnChanges) { bindingRecords.push(BindingRecord.createDirectiveOnChanges(directiveRecord)); } @@ -178,39 +171,29 @@ class ProtoViewVisitor implements TemplateAstVisitor { // TODO: these setters should eventually be created by change detection, to make // it monomorphic! var setter = reflector.setter(ast.directiveName); - this.currentProtoView.bindingRecords.push( + this.bindingRecords.push( BindingRecord.createForDirective(ast.value, ast.directiveName, setter, directiveRecord)); return null; } } -class ProtoViewVisitorData { - boundTextCount: number = 0; - boundElementCount: number = 0; - variableNames: string[] = []; - bindingRecords: BindingRecord[] = []; - eventRecords: BindingRecord[] = []; - directiveRecords: DirectiveRecord[] = []; - constructor(public parent: ProtoViewVisitorData, public strategy: ChangeDetectionStrategy, - public viewIndex: number) {} -} -function createChangeDefinitions(pvDatas: ProtoViewVisitorData[], componentType: TypeMetadata, +function createChangeDefinitions(pvVisitors: ProtoViewVisitor[], componentType: TypeMetadata, genConfig: ChangeDetectorGenConfig): ChangeDetectorDefinition[] { - var pvVariableNames = _collectNestedProtoViewsVariableNames(pvDatas); - return pvDatas.map(pvData => { - var viewType = pvData.viewIndex === 0 ? 'component' : 'embedded'; - var id = _protoViewId(componentType, pvData.viewIndex, viewType); - return new ChangeDetectorDefinition(id, pvData.strategy, pvVariableNames[pvData.viewIndex], - pvData.bindingRecords, pvData.eventRecords, - pvData.directiveRecords, genConfig); + var pvVariableNames = _collectNestedProtoViewsVariableNames(pvVisitors); + return pvVisitors.map(pvVisitor => { + var viewType = pvVisitor.viewIndex === 0 ? 'component' : 'embedded'; + var id = _protoViewId(componentType, pvVisitor.viewIndex, viewType); + return new ChangeDetectorDefinition( + id, pvVisitor.strategy, pvVariableNames[pvVisitor.viewIndex], pvVisitor.bindingRecords, + pvVisitor.eventRecords, pvVisitor.directiveRecords, genConfig); }); } -function _collectNestedProtoViewsVariableNames(pvs: ProtoViewVisitorData[]): string[][] { - var nestedPvVariableNames: string[][] = ListWrapper.createFixedSize(pvs.length); - pvs.forEach((pv) => { +function _collectNestedProtoViewsVariableNames(pvVisitors: ProtoViewVisitor[]): string[][] { + var nestedPvVariableNames: string[][] = ListWrapper.createFixedSize(pvVisitors.length); + pvVisitors.forEach((pv) => { var parentVariableNames: string[] = isPresent(pv.parent) ? nestedPvVariableNames[pv.parent.viewIndex] : []; nestedPvVariableNames[pv.viewIndex] = parentVariableNames.concat(pv.variableNames); diff --git a/modules/angular2/src/compiler/command_compiler.ts b/modules/angular2/src/compiler/command_compiler.ts new file mode 100644 index 0000000000..533bc7579d --- /dev/null +++ b/modules/angular2/src/compiler/command_compiler.ts @@ -0,0 +1,241 @@ +import {isPresent, Type} from 'angular2/src/core/facade/lang'; +import { + TemplateCmd, + text, + ngContent, + beginElement, + endElement, + beginComponent, + endComponent, + embeddedTemplate +} from 'angular2/src/core/compiler/template_commands'; +import { + TemplateAst, + TemplateAstVisitor, + NgContentAst, + EmbeddedTemplateAst, + ElementAst, + VariableAst, + BoundEventAst, + BoundElementPropertyAst, + AttrAst, + BoundTextAst, + TextAst, + DirectiveAst, + BoundDirectivePropertyAst, + templateVisitAll +} from './template_ast'; +import {SourceModule, DirectiveMetadata, TypeMetadata} from './api'; +import {ViewEncapsulation} from 'angular2/src/core/render/api'; +import {shimHostAttribute, shimContentAttribute} from './style_compiler'; +import {escapeSingleQuoteString} from './util'; + +const TEMPLATE_COMMANDS_MODULE = 'angular2/src/core/compiler/template_commands'; +const TEMPLATE_COMMANDS_MODULE_ALIAS = 'tc'; + +export class CommandCompiler { + compileComponentRuntime(component: DirectiveMetadata, template: TemplateAst[], + componentTemplateFactory: Function): TemplateCmd[] { + var visitor = + new CommandBuilderVisitor(new RuntimeCommandFactory(componentTemplateFactory), component); + templateVisitAll(visitor, template); + return visitor.result; + } + + compileComponentCodeGen(component: DirectiveMetadata, template: TemplateAst[], + componentTemplateFactory: Function): SourceModule { + var imports: string[][] = [[TEMPLATE_COMMANDS_MODULE, TEMPLATE_COMMANDS_MODULE_ALIAS]]; + var visitor = new CommandBuilderVisitor( + new CodegenCommandFactory(componentTemplateFactory, TEMPLATE_COMMANDS_MODULE_ALIAS, + imports), + component); + templateVisitAll(visitor, template); + var source = `var COMMANDS = [${visitor.result.join(',')}];`; + return new SourceModule(null, source, imports); + } +} + +interface CommandFactory { + createText(value: string, isBound: boolean, ngContentIndex: number): R; + createNgContent(ngContentIndex: number): R; + createBeginElement(name: string, attrNameAndValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: TypeMetadata[], isBound: boolean, + ngContentIndex: number): R; + createEndElement(): R; + createBeginComponent(name: string, attrNameAndValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: TypeMetadata[], + nativeShadow: boolean, ngContentIndex: number): R; + createEndComponent(): R; + createEmbeddedTemplate(attrNameAndValues: string[], variableNameAndValues: string[], + directives: TypeMetadata[], isMerged: boolean, ngContentIndex: number, + children: R[]): R; +} + +class RuntimeCommandFactory implements CommandFactory { + constructor(public componentTemplateFactory: Function) {} + private _mapDirectives(directives: TypeMetadata[]): Type[] { + return directives.map(directive => directive.type); + } + + createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd { + return text(value, isBound, ngContentIndex); + } + createNgContent(ngContentIndex: number): TemplateCmd { return ngContent(ngContentIndex); } + createBeginElement(name: string, attrNameAndValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: TypeMetadata[], isBound: boolean, + ngContentIndex: number): TemplateCmd { + return beginElement(name, attrNameAndValues, eventNames, variableNameAndValues, + this._mapDirectives(directives), isBound, ngContentIndex); + } + createEndElement(): TemplateCmd { return endElement(); } + createBeginComponent(name: string, attrNameAndValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: TypeMetadata[], + nativeShadow: boolean, ngContentIndex: number): TemplateCmd { + return beginComponent(name, attrNameAndValues, eventNames, variableNameAndValues, + this._mapDirectives(directives), nativeShadow, ngContentIndex, + this.componentTemplateFactory(directives[0])); + } + createEndComponent(): TemplateCmd { return endComponent(); } + createEmbeddedTemplate(attrNameAndValues: string[], variableNameAndValues: string[], + directives: TypeMetadata[], isMerged: boolean, ngContentIndex: number, + children: TemplateCmd[]): TemplateCmd { + return embeddedTemplate(attrNameAndValues, variableNameAndValues, + this._mapDirectives(directives), isMerged, ngContentIndex, children); + } +} + +function escapeStringArray(data: string[]): string { + return `[${data.map( value => escapeSingleQuoteString(value)).join(',')}]`; +} + +class CodegenCommandFactory implements CommandFactory { + constructor(public componentTemplateFactory: Function, public templateCommandsModuleAlias, + public imports: string[][]) {} + + private _escapeDirectives(directives: TypeMetadata[]): string[] { + return directives.map(directiveType => { + var importAlias = `dir${this.imports.length}`; + this.imports.push([directiveType.typeUrl, importAlias]); + return `${importAlias}.${directiveType.typeName}`; + }); + } + + createText(value: string, isBound: boolean, ngContentIndex: number): string { + return `${this.templateCommandsModuleAlias}.text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`; + } + createNgContent(ngContentIndex: number): string { + return `${this.templateCommandsModuleAlias}.ngContent(${ngContentIndex})`; + } + createBeginElement(name: string, attrNameAndValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: TypeMetadata[], isBound: boolean, + ngContentIndex: number): string { + return `${this.templateCommandsModuleAlias}.beginElement(${escapeSingleQuoteString(name)}, ${escapeStringArray(attrNameAndValues)}, ${escapeStringArray(eventNames)}, ${escapeStringArray(variableNameAndValues)}, [${this._escapeDirectives(directives).join(',')}], ${isBound}, ${ngContentIndex})`; + } + createEndElement(): string { return `${this.templateCommandsModuleAlias}.endElement()`; } + createBeginComponent(name: string, attrNameAndValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: TypeMetadata[], + nativeShadow: boolean, ngContentIndex: number): string { + return `${this.templateCommandsModuleAlias}.beginComponent(${escapeSingleQuoteString(name)}, ${escapeStringArray(attrNameAndValues)}, ${escapeStringArray(eventNames)}, ${escapeStringArray(variableNameAndValues)}, [${this._escapeDirectives(directives).join(',')}], ${nativeShadow}, ${ngContentIndex}, ${this.componentTemplateFactory(directives[0], this.imports)})`; + } + createEndComponent(): string { return `${this.templateCommandsModuleAlias}.endComponent()`; } + createEmbeddedTemplate(attrNameAndValues: string[], variableNameAndValues: string[], + directives: TypeMetadata[], isMerged: boolean, ngContentIndex: number, + children: string[]): string { + return `${this.templateCommandsModuleAlias}.embeddedTemplate(${escapeStringArray(attrNameAndValues)}, ${escapeStringArray(variableNameAndValues)}, [${this._escapeDirectives(directives).join(',')}], ${isMerged}, ${ngContentIndex}, [${children.join(',')}])`; + } +} + +function visitAndReturnContext(visitor: TemplateAstVisitor, asts: TemplateAst[], context: any): + any { + templateVisitAll(visitor, asts, context); + return context; +} + +class CommandBuilderVisitor implements TemplateAstVisitor { + result: R[] = []; + transitiveNgContentCount: number = 0; + constructor(public commandFactory: CommandFactory, public component: DirectiveMetadata) {} + + private _readAttrNameAndValues(localComponent: DirectiveMetadata, + attrAsts: TemplateAst[]): string[] { + var attrNameAndValues: string[] = visitAndReturnContext(this, attrAsts, []); + if (isPresent(localComponent) && + localComponent.template.encapsulation === ViewEncapsulation.Emulated) { + attrNameAndValues.push(shimHostAttribute(localComponent.type)); + attrNameAndValues.push(''); + } + if (this.component.template.encapsulation === ViewEncapsulation.Emulated) { + attrNameAndValues.push(shimContentAttribute(this.component.type)); + attrNameAndValues.push(''); + } + return attrNameAndValues; + } + + visitNgContent(ast: NgContentAst, context: any): any { + this.transitiveNgContentCount++; + this.result.push(this.commandFactory.createNgContent(ast.ngContentIndex)); + return null; + } + visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { + var childVisitor = new CommandBuilderVisitor(this.commandFactory, this.component); + templateVisitAll(childVisitor, ast.children); + var isMerged = childVisitor.transitiveNgContentCount > 0; + this.transitiveNgContentCount += childVisitor.transitiveNgContentCount; + var directivesAndEventNames = visitAndReturnContext(this, ast.directives, [[], []]); + this.result.push(this.commandFactory.createEmbeddedTemplate( + this._readAttrNameAndValues(null, ast.attrs), visitAndReturnContext(this, ast.vars, []), + directivesAndEventNames[0], isMerged, ast.ngContentIndex, childVisitor.result)); + return null; + } + visitElement(ast: ElementAst, context: any): any { + var component = ast.getComponent(); + var eventNames = visitAndReturnContext(this, ast.events, []); + var directives = []; + visitAndReturnContext(this, ast.directives, [directives, eventNames]); + var attrNameAndValues = this._readAttrNameAndValues(component, ast.attrs); + var vars = visitAndReturnContext(this, ast.vars, []); + if (isPresent(component)) { + this.result.push(this.commandFactory.createBeginComponent( + ast.name, attrNameAndValues, eventNames, vars, directives, + component.template.encapsulation === ViewEncapsulation.Native, ast.ngContentIndex)); + templateVisitAll(this, ast.children); + this.result.push(this.commandFactory.createEndComponent()); + } else { + this.result.push(this.commandFactory.createBeginElement(ast.name, attrNameAndValues, + eventNames, vars, directives, + ast.isBound(), ast.ngContentIndex)); + templateVisitAll(this, ast.children); + this.result.push(this.commandFactory.createEndElement()); + } + return null; + } + visitVariable(ast: VariableAst, variableNameAndValues: string[]): any { + variableNameAndValues.push(ast.name); + variableNameAndValues.push(ast.value); + return null; + } + visitAttr(ast: AttrAst, attrNameAndValues: string[]): any { + attrNameAndValues.push(ast.name); + attrNameAndValues.push(ast.value); + return null; + } + visitBoundText(ast: BoundTextAst, context: any): any { + this.result.push(this.commandFactory.createText(null, true, ast.ngContentIndex)); + return null; + } + visitText(ast: TextAst, context: any): any { + this.result.push(this.commandFactory.createText(ast.value, false, ast.ngContentIndex)); + return null; + } + visitDirective(ast: DirectiveAst, directivesAndEventNames: any[][]): any { + directivesAndEventNames[0].push(ast.directive.type); + templateVisitAll(this, ast.hostEvents, directivesAndEventNames[1]); + return null; + } + visitEvent(ast: BoundEventAst, eventNames: string[]): any { + eventNames.push(ast.getFullName()); + return null; + } + visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; } + visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; } +} diff --git a/modules/angular2/src/compiler/style_compiler.ts b/modules/angular2/src/compiler/style_compiler.ts index 085993c69b..874610bb9c 100644 --- a/modules/angular2/src/compiler/style_compiler.ts +++ b/modules/angular2/src/compiler/style_compiler.ts @@ -1,16 +1,17 @@ -import {DirectiveMetadata, SourceModule, ViewEncapsulation} from './api'; +import {DirectiveMetadata, SourceModule, TypeMetadata} from './api'; +import {ViewEncapsulation} from 'angular2/src/core/render/api'; import {XHR} from 'angular2/src/core/render/xhr'; import {StringWrapper, isJsObject, isBlank} from 'angular2/src/core/facade/lang'; import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async'; import {ShadowCss} from 'angular2/src/core/render/dom/compiler/shadow_css'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; import {resolveStyleUrls} from './style_url_resolver'; +import {escapeSingleQuoteString} from './util'; const COMPONENT_VARIABLE = '%COMP%'; var COMPONENT_REGEX = /%COMP%/g; const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; -var ESCAPE_STRING_RE = /'|\\|\n/g; var IS_DART = !isJsObject({}); export class StyleCompiler { @@ -78,7 +79,7 @@ export class StyleCompiler { var imports: string[][] = []; var moduleSource = `var STYLES = (`; moduleSource += - `[${plainStyles.map( plainStyle => escapeString(this._shimIfNeeded(plainStyle, shim)) ).join(',')}]`; + `[${plainStyles.map( plainStyle => escapeSingleQuoteString(this._shimIfNeeded(plainStyle, shim)) ).join(',')}]`; for (var i = 0; i < absUrls.length; i++) { var url = absUrls[i]; var moduleAlias = `import${i}`; @@ -98,15 +99,12 @@ export class StyleCompiler { } } -function escapeString(input: string): string { - var escapedInput = StringWrapper.replaceAllMapped(input, ESCAPE_STRING_RE, (match) => { - if (match[0] == "'" || match[0] == '\\') { - return `\\${match[0]}`; - } else { - return '\\n'; - } - }); - return `'${escapedInput}'`; +export function shimContentAttribute(component: TypeMetadata): string { + return StringWrapper.replaceAll(CONTENT_ATTR, COMPONENT_REGEX, `${component.id}`); +} + +export function shimHostAttribute(component: TypeMetadata): string { + return StringWrapper.replaceAll(HOST_ATTR, COMPONENT_REGEX, `${component.id}`); } function codeGenConcatArray(expression: string): string { diff --git a/modules/angular2/src/compiler/template_ast.ts b/modules/angular2/src/compiler/template_ast.ts index 38085f6674..7f293383af 100644 --- a/modules/angular2/src/compiler/template_ast.ts +++ b/modules/angular2/src/compiler/template_ast.ts @@ -8,12 +8,12 @@ export interface TemplateAst { } export class TextAst implements TemplateAst { - constructor(public value: string, public sourceInfo: string) {} + constructor(public value: string, public ngContentIndex: number, public sourceInfo: string) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitText(this, context); } } export class BoundTextAst implements TemplateAst { - constructor(public value: AST, public sourceInfo: string) {} + constructor(public value: AST, public ngContentIndex: number, public sourceInfo: string) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitBoundText(this, context); } @@ -38,6 +38,13 @@ export class BoundEventAst implements TemplateAst { visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitEvent(this, context); } + getFullName(): string { + if (isPresent(this.target)) { + return `${this.target}:${this.name}`; + } else { + return this.name; + } + } } export class VariableAst implements TemplateAst { @@ -48,9 +55,10 @@ export class VariableAst implements TemplateAst { } export class ElementAst implements TemplateAst { - constructor(public attrs: AttrAst[], public properties: BoundElementPropertyAst[], - public events: BoundEventAst[], public vars: VariableAst[], - public directives: DirectiveAst[], public children: TemplateAst[], + constructor(public name: string, public attrs: AttrAst[], + public properties: BoundElementPropertyAst[], public events: BoundEventAst[], + public vars: VariableAst[], public directives: DirectiveAst[], + public children: TemplateAst[], public ngContentIndex: number, public sourceInfo: string) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitElement(this, context); @@ -60,12 +68,18 @@ export class ElementAst implements TemplateAst { return (this.properties.length > 0 || this.events.length > 0 || this.vars.length > 0 || this.directives.length > 0); } + + getComponent(): DirectiveMetadata { + return this.directives.length > 0 && this.directives[0].directive.isComponent ? + this.directives[0].directive : + null; + } } export class EmbeddedTemplateAst implements TemplateAst { constructor(public attrs: AttrAst[], public vars: VariableAst[], public directives: DirectiveAst[], public children: TemplateAst[], - public sourceInfo: string) {} + public ngContentIndex: number, public sourceInfo: string) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitEmbeddedTemplate(this, context); } @@ -89,7 +103,7 @@ export class DirectiveAst implements TemplateAst { } export class NgContentAst implements TemplateAst { - constructor(public select: string, public sourceInfo: string) {} + constructor(public ngContentIndex: number, public sourceInfo: string) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitNgContent(this, context); } diff --git a/modules/angular2/src/compiler/template_parser.ts b/modules/angular2/src/compiler/template_parser.ts index a6bec134b5..8e3ccab3b4 100644 --- a/modules/angular2/src/compiler/template_parser.ts +++ b/modules/angular2/src/compiler/template_parser.ts @@ -12,7 +12,7 @@ import {BaseException} from 'angular2/src/core/facade/exceptions'; import {Parser, AST, ASTWithSource} from 'angular2/src/core/change_detection/change_detection'; import {TemplateBinding} from 'angular2/src/core/change_detection/parser/ast'; -import {DirectiveMetadata} from './api'; +import {DirectiveMetadata, TemplateMetadata} from './api'; import { ElementAst, BoundElementPropertyAst, @@ -54,7 +54,6 @@ import {dashCaseToCamelCase, camelCaseToDashCase} from './util'; var BIND_NAME_REGEXP = /^(?:(?:(?:(bind-)|(var-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g; -const NG_CONTENT_SELECT_ATTR = 'select'; const NG_CONTENT_ELEMENT = 'ng-content'; const TEMPLATE_ELEMENT = 'template'; const TEMPLATE_ATTR = 'template'; @@ -67,12 +66,14 @@ const ATTRIBUTE_PREFIX = 'attr'; const CLASS_PREFIX = 'class'; const STYLE_PREFIX = 'style'; +var TEXT_CSS_SELECTOR = CssSelector.parse('*')[0]; + export class TemplateParser { constructor(private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry) {} parse(domNodes: HtmlAst[], directives: DirectiveMetadata[]): TemplateAst[] { var parseVisitor = new TemplateParseVisitor(directives, this._exprParser, this._schemaRegistry); - var result = htmlVisitAll(parseVisitor, domNodes); + var result = htmlVisitAll(parseVisitor, domNodes, EMPTY_COMPONENT); if (parseVisitor.errors.length > 0) { var errorString = parseVisitor.errors.join('\n'); throw new BaseException(`Template parse errors:\n${errorString}`); @@ -131,18 +132,21 @@ class TemplateParseVisitor implements HtmlAstVisitor { } } - visitText(ast: HtmlTextAst): any { + visitText(ast: HtmlTextAst, component: Component): any { + var ngContentIndex = component.findNgContentIndex(TEXT_CSS_SELECTOR); var expr = this._parseInterpolation(ast.value, ast.sourceInfo); if (isPresent(expr)) { - return new BoundTextAst(expr, ast.sourceInfo); + return new BoundTextAst(expr, ngContentIndex, ast.sourceInfo); } else { - return new TextAst(ast.value, ast.sourceInfo); + return new TextAst(ast.value, ngContentIndex, ast.sourceInfo); } } - visitAttr(ast: HtmlAttrAst): any { return new AttrAst(ast.name, ast.value, ast.sourceInfo); } + visitAttr(ast: HtmlAttrAst, contex: any): any { + return new AttrAst(ast.name, ast.value, ast.sourceInfo); + } - visitElement(element: HtmlElementAst): any { + visitElement(element: HtmlElementAst, component: Component): any { var nodeName = element.name; var matchableAttrs: string[][] = []; var elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = []; @@ -154,52 +158,53 @@ class TemplateParseVisitor implements HtmlAstVisitor { var templateMatchableAttrs: string[][] = []; var hasInlineTemplates = false; var attrs = []; - var selectAttr = null; element.attrs.forEach(attr => { matchableAttrs.push([attr.name, attr.value]); - if (attr.name == NG_CONTENT_SELECT_ATTR) { - selectAttr = attr.value; - } var hasBinding = this._parseAttr(attr, matchableAttrs, elementOrDirectiveProps, events, vars); var hasTemplateBinding = this._parseInlineTemplateBinding( attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateVars); if (!hasBinding && !hasTemplateBinding) { // don't include the bindings as attributes as well in the AST - attrs.push(this.visitAttr(attr)); + attrs.push(this.visitAttr(attr, null)); } if (hasTemplateBinding) { hasInlineTemplates = true; } }); + var elementCssSelector = this._createElementCssSelector(nodeName, matchableAttrs); var directives = this._createDirectiveAsts( - element.name, this._parseDirectives(this.selectorMatcher, nodeName, matchableAttrs), + element.name, this._parseDirectives(this.selectorMatcher, elementCssSelector), elementOrDirectiveProps, element.sourceInfo); var elementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directives); - var children = htmlVisitAll(this, element.children); + var children = htmlVisitAll(this, element.children, Component.create(directives)); + var elementNgContentIndex = + hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector); var parsedElement; if (nodeName == NG_CONTENT_ELEMENT) { - parsedElement = new NgContentAst(selectAttr, element.sourceInfo); + parsedElement = new NgContentAst(elementNgContentIndex, element.sourceInfo); } else if (nodeName == TEMPLATE_ELEMENT) { this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps, events, element.sourceInfo); - parsedElement = - new EmbeddedTemplateAst(attrs, vars, directives, children, element.sourceInfo); + parsedElement = new EmbeddedTemplateAst(attrs, vars, directives, children, + elementNgContentIndex, element.sourceInfo); } else { this._assertOnlyOneComponent(directives, element.sourceInfo); - parsedElement = new ElementAst(attrs, elementProps, events, vars, directives, children, - element.sourceInfo); + parsedElement = new ElementAst(nodeName, attrs, elementProps, events, vars, directives, + children, elementNgContentIndex, element.sourceInfo); } if (hasInlineTemplates) { + var templateCssSelector = + this._createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs); var templateDirectives = this._createDirectiveAsts( - element.name, - this._parseDirectives(this.selectorMatcher, TEMPLATE_ELEMENT, templateMatchableAttrs), + element.name, this._parseDirectives(this.selectorMatcher, templateCssSelector), templateElementOrDirectiveProps, element.sourceInfo); var templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts( element.name, templateElementOrDirectiveProps, templateDirectives); this._assertNoComponentsNorElementBindingsOnTemplate(templateDirectives, templateElementProps, [], element.sourceInfo); parsedElement = new EmbeddedTemplateAst([], templateVars, templateDirectives, [parsedElement], + component.findNgContentIndex(templateCssSelector), element.sourceInfo); } return parsedElement; @@ -349,8 +354,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { sourceInfo)); } - private _parseDirectives(selectorMatcher: SelectorMatcher, elementName: string, - matchableAttrs: string[][]): DirectiveMetadata[] { + private _createElementCssSelector(elementName: string, matchableAttrs: string[][]): CssSelector { var cssSelector = new CssSelector(); cssSelector.setElement(elementName); @@ -363,8 +367,14 @@ class TemplateParseVisitor implements HtmlAstVisitor { classes.forEach(className => cssSelector.addClassName(className)); } } + return cssSelector; + } + + private _parseDirectives(selectorMatcher: SelectorMatcher, + elementCssSelector: CssSelector): DirectiveMetadata[] { var directives = []; - selectorMatcher.match(cssSelector, (selector, directive) => { directives.push(directive); }); + selectorMatcher.match(elementCssSelector, + (selector, directive) => { directives.push(directive); }); // Need to sort the directives so that we get consistent results throughout, // as selectorMatcher uses Maps inside. // Also need to make components the first directive in the array @@ -516,9 +526,10 @@ class TemplateParseVisitor implements HtmlAstVisitor { } } - _assertNoComponentsNorElementBindingsOnTemplate(directives: DirectiveAst[], - elementProps: BoundElementPropertyAst[], - events: BoundEventAst[], sourceInfo: string) { + private _assertNoComponentsNorElementBindingsOnTemplate(directives: DirectiveAst[], + elementProps: BoundElementPropertyAst[], + events: BoundEventAst[], + sourceInfo: string) { var componentTypeNames: string[] = this._findComponentDirectiveNames(directives); if (componentTypeNames.length > 0) { this._reportError( @@ -555,4 +566,39 @@ export function splitAtColon(input: string, defaultValues: string[]): string[] { } else { return defaultValues; } -} \ No newline at end of file +} + +class Component { + static create(directives: DirectiveAst[]): Component { + if (directives.length === 0 || !directives[0].directive.isComponent) { + return EMPTY_COMPONENT; + } + var matcher = new SelectorMatcher(); + var ngContentSelectors = directives[0].directive.template.ngContentSelectors; + var wildcardNgContentIndex = null; + for (var i = 0; i < ngContentSelectors.length; i++) { + var selector = ngContentSelectors[i]; + if (StringWrapper.equals(selector, '*')) { + wildcardNgContentIndex = i; + } else { + matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i); + } + } + return new Component(matcher, wildcardNgContentIndex); + } + constructor(public ngContentIndexMatcher: SelectorMatcher, + public wildcardNgContentIndex: number) {} + + findNgContentIndex(selector: CssSelector): number { + var ngContentIndices = []; + if (isPresent(this.wildcardNgContentIndex)) { + ngContentIndices.push(this.wildcardNgContentIndex); + } + this.ngContentIndexMatcher.match( + selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); }); + ListWrapper.sort(ngContentIndices); + return ngContentIndices.length > 0 ? ngContentIndices[0] : null; + } +} + +var EMPTY_COMPONENT = new Component(new SelectorMatcher(), null); diff --git a/modules/angular2/src/compiler/util.ts b/modules/angular2/src/compiler/util.ts index 1ba3c4fdad..7205b543eb 100644 --- a/modules/angular2/src/compiler/util.ts +++ b/modules/angular2/src/compiler/util.ts @@ -1,8 +1,9 @@ -import {StringWrapper} from 'angular2/src/core/facade/lang'; +import {StringWrapper, isBlank} from 'angular2/src/core/facade/lang'; var CAMEL_CASE_REGEXP = /([A-Z])/g; var DASH_CASE_REGEXP = /-([a-z])/g; - +var SINGLE_QUOTE_ESCAPE_STRING_RE = /'|\\|\n/g; +var DOUBLE_QUOTE_ESCAPE_STRING_RE = /"|\\|\n/g; export function camelCaseToDashCase(input: string): string { return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, @@ -13,3 +14,27 @@ export function dashCaseToCamelCase(input: string): string { return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => { return m[1].toUpperCase(); }); } + +export function escapeSingleQuoteString(input: string): string { + if (isBlank(input)) { + return null; + } + return `'${escapeString(input, SINGLE_QUOTE_ESCAPE_STRING_RE)}'`; +} + +export function escapeDoubleQuoteString(input: string): string { + if (isBlank(input)) { + return null; + } + return `"${escapeString(input, DOUBLE_QUOTE_ESCAPE_STRING_RE)}"`; +} + +function escapeString(input: string, re: RegExp): string { + return StringWrapper.replaceAllMapped(input, re, (match) => { + if (match[0] == '\n') { + return '\\n'; + } else { + return `\\${match[0]}`; + } + }); +} diff --git a/modules/angular2/src/core/compiler/template_commands.ts b/modules/angular2/src/core/compiler/template_commands.ts new file mode 100644 index 0000000000..188c101f05 --- /dev/null +++ b/modules/angular2/src/core/compiler/template_commands.ts @@ -0,0 +1,141 @@ +import {Type, CONST_EXPR, isPresent} from 'angular2/src/core/facade/lang'; +import { + RenderTemplateCmd, + RenderCommandVisitor, + RenderBeginElementCmd, + RenderTextCmd, + RenderNgContentCmd, + RenderBeginComponentCmd, + RenderEmbeddedTemplateCmd +} from 'angular2/src/core/render/render'; + +export class CompiledTemplate { + constructor(public id: string, public commands: TemplateCmd[]) {} +} + +const EMPTY_ARR = CONST_EXPR([]); + +export interface TemplateCmd extends RenderTemplateCmd { + visit(visitor: CommandVisitor, context: any): any; +} + +export class TextCmd implements TemplateCmd, RenderTextCmd { + constructor(public value: string, public isBound: boolean, public ngContentIndex: number) {} + visit(visitor: CommandVisitor, context: any): any { return visitor.visitText(this, context); } +} + +export function text(value: string, isBound: boolean, ngContentIndex: number): TextCmd { + return new TextCmd(value, isBound, ngContentIndex); +} + +export class NgContentCmd implements TemplateCmd, RenderNgContentCmd { + isBound: boolean = false; + constructor(public ngContentIndex: number) {} + visit(visitor: CommandVisitor, context: any): any { + return visitor.visitNgContent(this, context); + } +} + +export function ngContent(ngContentIndex: number): NgContentCmd { + return new NgContentCmd(ngContentIndex); +} + +export interface IBeginElementCmd extends TemplateCmd, RenderBeginElementCmd { + variableNameAndValues: string[]; + eventNames: string[]; + directives: Type[]; + visit(visitor: CommandVisitor, context: any): any; +} + +export class BeginElementCmd implements TemplateCmd, IBeginElementCmd, RenderBeginElementCmd { + constructor(public name: string, public attrNameAndValues: string[], public eventNames: string[], + public variableNameAndValues: string[], public directives: Type[], + public isBound: boolean, public ngContentIndex: number) {} + visit(visitor: CommandVisitor, context: any): any { + return visitor.visitBeginElement(this, context); + } +} + +export function beginElement(name: string, attrNameAndValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: Type[], isBound: boolean, + ngContentIndex: number): BeginElementCmd { + return new BeginElementCmd(name, attrNameAndValues, eventNames, variableNameAndValues, directives, + isBound, ngContentIndex); +} + +export class EndElementCmd implements TemplateCmd { + visit(visitor: CommandVisitor, context: any): any { return visitor.visitEndElement(context); } +} + +export function endElement(): TemplateCmd { + return new EndElementCmd(); +} + +export class BeginComponentCmd implements TemplateCmd, IBeginElementCmd, RenderBeginComponentCmd { + isBound: boolean = true; + templateId: string; + component: Type; + constructor(public name: string, public attrNameAndValues: string[], public eventNames: string[], + public variableNameAndValues: string[], public directives: Type[], + public nativeShadow: boolean, public ngContentIndex: number, + public template: CompiledTemplate) { + this.component = directives[0]; + this.templateId = isPresent(template) ? template.id : null; + } + visit(visitor: CommandVisitor, context: any): any { + return visitor.visitBeginComponent(this, context); + } +} + +export function beginComponent(name: string, attrNameAnsValues: string[], eventNames: string[], + variableNameAndValues: string[], directives: Type[], + nativeShadow: boolean, ngContentIndex: number, + template: CompiledTemplate): BeginComponentCmd { + return new BeginComponentCmd(name, attrNameAnsValues, eventNames, variableNameAndValues, + directives, nativeShadow, ngContentIndex, template); +} + +export class EndComponentCmd implements TemplateCmd { + visit(visitor: CommandVisitor, context: any): any { return visitor.visitEndComponent(context); } +} + +export function endComponent(): TemplateCmd { + return new EndComponentCmd(); +} + +export class EmbeddedTemplateCmd implements TemplateCmd, IBeginElementCmd, + RenderEmbeddedTemplateCmd { + isBound: boolean = true; + name: string = null; + eventNames: string[] = EMPTY_ARR; + constructor(public attrNameAndValues: string[], public variableNameAndValues: string[], + public directives: Type[], public isMerged: boolean, public ngContentIndex: number, + public children: TemplateCmd[]) {} + visit(visitor: CommandVisitor, context: any): any { + return visitor.visitEmbeddedTemplate(this, context); + } +} + +export function embeddedTemplate(attrNameAndValues: string[], variableNameAndValues: string[], + directives: Type[], isMerged: boolean, ngContentIndex: number, + children: TemplateCmd[]): EmbeddedTemplateCmd { + return new EmbeddedTemplateCmd(attrNameAndValues, variableNameAndValues, directives, isMerged, + ngContentIndex, children); +} + +export interface CommandVisitor extends RenderCommandVisitor { + visitText(cmd: TextCmd, context: any): any; + visitNgContent(cmd: NgContentCmd, context: any): any; + visitBeginElement(cmd: BeginElementCmd, context: any): any; + visitEndElement(context: any): any; + visitBeginComponent(cmd: BeginComponentCmd, context: any): any; + visitEndComponent(context: any): any; + visitEmbeddedTemplate(cmd: EmbeddedTemplateCmd, context: any): any; +} + +export function visitAllCommands(visitor: CommandVisitor, cmds: TemplateCmd[], + context: any = null) { + for (var i = 0; i < cmds.length; i++) { + cmds[i].visit(visitor, context); + } +} diff --git a/modules/angular2/src/core/render/api.ts b/modules/angular2/src/core/render/api.ts index 0b6729e303..809f047a09 100644 --- a/modules/angular2/src/core/render/api.ts +++ b/modules/angular2/src/core/render/api.ts @@ -1,4 +1,4 @@ -import {isPresent, isBlank, RegExpWrapper} from 'angular2/src/core/facade/lang'; +import {isPresent, isBlank, RegExpWrapper, deserializeEnum} from 'angular2/src/core/facade/lang'; import {Promise} from 'angular2/src/core/facade/async'; import {Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/core/facade/collection'; import { @@ -390,6 +390,45 @@ export class RenderCompiler { } } +export interface RenderTemplateCmd { visit(visitor: RenderCommandVisitor, context: any): any; } + +export interface RenderBeginCmd extends RenderTemplateCmd { + ngContentIndex: number; + isBound: boolean; +} + +export interface RenderTextCmd extends RenderBeginCmd { value: string; } + +export interface RenderNgContentCmd extends RenderBeginCmd { ngContentIndex: number; } + +export interface RenderBeginElementCmd extends RenderBeginCmd { + name: string; + attrNameAndValues: string[]; + eventNames: string[]; +} + +export interface RenderBeginComponentCmd extends RenderBeginElementCmd { + nativeShadow: boolean; + templateId: string; +} + +export interface RenderEmbeddedTemplateCmd extends RenderBeginElementCmd { + isMerged: boolean; + children: RenderTemplateCmd[]; +} + +// TODO(tbosch): change ts2dart to allow to use `CMD` as type in these methods! +export interface RenderCommandVisitor { + visitText /**/ (cmd: any, context: any): any; + visitNgContent /**/ (cmd: any, context: any): any; + visitBeginElement /**/ (cmd: any, context: any): any; + visitEndElement(context: any): any; + visitBeginComponent /**/ (cmd: any, context: any): any; + visitEndComponent(context: any): any; + visitEmbeddedTemplate /**/ (cmd: any, context: any): any; +} + + export class RenderViewWithFragments { constructor(public viewRef: RenderViewRef, public fragmentRefs: RenderFragmentRef[]) {} } diff --git a/modules/angular2/test/compiler/command_compiler_spec.ts b/modules/angular2/test/compiler/command_compiler_spec.ts new file mode 100644 index 0000000000..810b248801 --- /dev/null +++ b/modules/angular2/test/compiler/command_compiler_spec.ts @@ -0,0 +1,489 @@ +import { + ddescribe, + describe, + xdescribe, + it, + iit, + xit, + expect, + beforeEach, + afterEach, + AsyncTestCompleter, + inject +} from 'angular2/test_lib'; + +import {IS_DART} from '../platform'; +import {CONST_EXPR, stringify, isType, Type, isBlank} from 'angular2/src/core/facade/lang'; +import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async'; +import {HtmlParser} from 'angular2/src/compiler/html_parser'; +import {TemplateParser} from 'angular2/src/compiler/template_parser'; +import {MockSchemaRegistry} from './template_parser_spec'; +import {Parser, Lexer} from 'angular2/src/core/change_detection/change_detection'; +import { + CommandVisitor, + TextCmd, + NgContentCmd, + BeginElementCmd, + BeginComponentCmd, + EmbeddedTemplateCmd, + TemplateCmd, + visitAllCommands, + CompiledTemplate +} from 'angular2/src/core/compiler/template_commands'; +import {CommandCompiler} from 'angular2/src/compiler/command_compiler'; +import { + DirectiveMetadata, + TypeMetadata, + TemplateMetadata, + SourceModule +} from 'angular2/src/compiler/api'; +import {ViewEncapsulation} from 'angular2/src/core/render/api'; +import {evalModule} from './eval_module'; +import {escapeSingleQuoteString} from 'angular2/src/compiler/util'; + +const BEGIN_ELEMENT = 'BEGIN_ELEMENT'; +const END_ELEMENT = 'END_ELEMENT'; +const BEGIN_COMPONENT = 'BEGIN_COMPONENT'; +const END_COMPONENT = 'END_COMPONENT'; +const TEXT = 'TEXT'; +const NG_CONTENT = 'NG_CONTENT'; +const EMBEDDED_TEMPLATE = 'EMBEDDED_TEMPLATE'; + +// Attention: These module names have to correspond to real modules! +const MODULE_NAME = 'angular2/test/compiler/command_compiler_spec'; +const TEMPLATE_COMMANDS_MODULE_NAME = 'angular2/src/core/compiler/template_commands'; + +// Attention: read by eval! +export class RootComp {} +export class SomeDir {} +export class AComp {} + +var RootCompTypeMeta = + new TypeMetadata({typeName: 'RootComp', id: 1, type: RootComp, typeUrl: MODULE_NAME}); +var SomeDirTypeMeta = + new TypeMetadata({typeName: 'SomeDir', id: 2, type: SomeDir, typeUrl: MODULE_NAME}); +var ACompTypeMeta = new TypeMetadata({typeName: 'AComp', id: 3, type: AComp, typeUrl: MODULE_NAME}); + +var NESTED_COMPONENT = new CompiledTemplate('someNestedComponentId', []); + +export function main() { + describe('CommandCompiler', () => { + var domParser: HtmlParser; + var parser: TemplateParser; + var commandCompiler: CommandCompiler; + var componentTemplateFactory: Function; + + beforeEach(() => { + domParser = new HtmlParser(); + parser = new TemplateParser( + new Parser(new Lexer()), + new MockSchemaRegistry({'invalidProp': false}, {'mappedAttr': 'mappedProp'})); + commandCompiler = new CommandCompiler(); + }); + + function createComp({type, selector, template, encapsulation, ngContentSelectors}: { + type?: TypeMetadata, + selector?: string, + template?: string, + encapsulation?: ViewEncapsulation, + ngContentSelectors?: string[] + }): DirectiveMetadata { + if (isBlank(encapsulation)) { + encapsulation = ViewEncapsulation.None; + } + if (isBlank(selector)) { + selector = 'root'; + } + if (isBlank(ngContentSelectors)) { + ngContentSelectors = []; + } + if (isBlank(template)) { + template = ''; + } + return new DirectiveMetadata({ + selector: selector, + isComponent: true, + type: type, + template: new TemplateMetadata({ + template: template, + ngContentSelectors: ngContentSelectors, + encapsulation: encapsulation + }) + }); + } + + function createDirective(type: TypeMetadata, selector: string): DirectiveMetadata { + return new DirectiveMetadata({selector: selector, isComponent: false, type: type}); + } + + + function createTests(run: Function) { + describe('text', () => { + + it('should create unbound text commands', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({type: RootCompTypeMeta, template: 'a'}); + run(rootComp, []) + .then((data) => { + expect(data).toEqual([[TEXT, 'a', false, null]]); + async.done(); + }); + })); + + it('should create bound text commands', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({type: RootCompTypeMeta, template: '{{a}}'}); + run(rootComp, []) + .then((data) => { + expect(data).toEqual([[TEXT, null, true, null]]); + async.done(); + }); + })); + + }); + + describe('elements', () => { + + it('should create unbound element commands', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({type: RootCompTypeMeta, template: '
'}); + run(rootComp, []) + .then((data) => { + expect(data).toEqual([ + [BEGIN_ELEMENT, 'div', ['a', 'b'], [], [], [], false, null], + [END_ELEMENT] + ]); + async.done(); + }); + })); + + it('should create bound element commands', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({ + type: RootCompTypeMeta, + template: '
' + }); + var dir = createDirective(SomeDirTypeMeta, '[a]'); + run(rootComp, [dir]) + .then((data) => { + expect(data).toEqual([ + [ + BEGIN_ELEMENT, + 'div', + ['a', 'b'], + ['click'], + ['someVar', 'someValue'], + ['SomeDirType'], + true, + null + ], + [END_ELEMENT] + ]); + async.done(); + }); + })); + + it('should emulate style encapsulation', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({ + type: RootCompTypeMeta, + template: '
', + encapsulation: ViewEncapsulation.Emulated + }); + run(rootComp, []) + .then((data) => { + expect(data).toEqual([ + [BEGIN_ELEMENT, 'div', ['_ngcontent-1', ''], [], [], [], false, null], + [END_ELEMENT] + ]); + async.done(); + }); + })); + + it('should create nested nodes', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({type: RootCompTypeMeta, template: '
a
'}); + run(rootComp, []) + .then((data) => { + expect(data).toEqual([ + [BEGIN_ELEMENT, 'div', [], [], [], [], false, null], + [TEXT, 'a', false, null], + [END_ELEMENT] + ]); + async.done(); + }); + })); + }); + + describe('components', () => { + + it('should create component commands', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({ + type: RootCompTypeMeta, + template: '' + }); + var comp = createComp({type: ACompTypeMeta, selector: 'a'}); + run(rootComp, [comp]) + .then((data) => { + expect(data).toEqual([ + [ + BEGIN_COMPONENT, + 'a', + ['a', 'b'], + ['click'], + ['someVar', 'someValue'], + ['ACompType'], + false, + null, + 'AComp' + ], + [END_COMPONENT] + ]); + async.done(); + }); + })); + + it('should emulate style encapsulation on host elements', + inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({ + type: RootCompTypeMeta, + template: '', + encapsulation: ViewEncapsulation.Emulated + }); + var comp = createComp( + {type: ACompTypeMeta, selector: 'a', encapsulation: ViewEncapsulation.Emulated}); + run(rootComp, [comp]) + .then((data) => { + expect(data).toEqual([ + [ + BEGIN_COMPONENT, + 'a', + ['_nghost-3', '', '_ngcontent-1', ''], + [], + [], + ['ACompType'], + false, + null, + 'AComp' + ], + [END_COMPONENT] + ]); + async.done(); + }); + })); + + it('should set nativeShadow flag', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({type: RootCompTypeMeta, template: ''}); + var comp = createComp( + {type: ACompTypeMeta, selector: 'a', encapsulation: ViewEncapsulation.Native}); + run(rootComp, [comp]) + .then((data) => { + expect(data).toEqual([ + [BEGIN_COMPONENT, 'a', [], [], [], ['ACompType'], true, null, 'AComp'], + [END_COMPONENT] + ]); + async.done(); + }); + })); + + it('should create nested nodes and set ngContentIndex', + inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({type: RootCompTypeMeta, template: 't'}); + var comp = createComp({type: ACompTypeMeta, selector: 'a', ngContentSelectors: ['*']}); + run(rootComp, [comp]) + .then((data) => { + expect(data).toEqual([ + [BEGIN_COMPONENT, 'a', [], [], [], ['ACompType'], false, null, 'AComp'], + [TEXT, 't', false, 0], + [END_COMPONENT] + ]); + async.done(); + }); + })); + }); + + describe('embedded templates', () => { + it('should create embedded template commands', inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({ + type: RootCompTypeMeta, + template: '' + }); + var dir = createDirective(SomeDirTypeMeta, '[a]'); + run(rootComp, [dir]) + .then((data) => { + expect(data).toEqual([ + [ + EMBEDDED_TEMPLATE, + ['a', 'b'], + ['someVar', 'someValue'], + ['SomeDirType'], + false, + null, + [] + ] + ]); + async.done(); + }); + })); + + it('should created nested nodes', inject([AsyncTestCompleter], (async) => { + var rootComp = + createComp({type: RootCompTypeMeta, template: ''}); + run(rootComp, []) + .then((data) => { + expect(data).toEqual( + [[EMBEDDED_TEMPLATE, [], [], [], false, null, [[TEXT, 't', false, null]]]]); + async.done(); + }); + })); + + it('should calculate wether the template is merged based on nested ng-content elements', + inject([AsyncTestCompleter], (async) => { + var rootComp = createComp({ + type: RootCompTypeMeta, + template: '' + }); + run(rootComp, []) + .then((data) => { + expect(data).toEqual( + [[EMBEDDED_TEMPLATE, [], [], [], true, null, [[NG_CONTENT, null]]]]); + async.done(); + }); + })); + + }); + + describe('ngContent', () => { + it('should create ng-content commands', inject([AsyncTestCompleter], (async) => { + var rootComp = + createComp({type: RootCompTypeMeta, template: ''}); + run(rootComp, []) + .then((data) => { + expect(data).toEqual([[NG_CONTENT, null]]); + async.done(); + }); + })); + }); + } + + describe('compileComponentRuntime', () => { + beforeEach(() => { + componentTemplateFactory = (directiveType: TypeMetadata) => { + return new CompiledTemplate(directiveType.typeName, []); + }; + }); + + function run(component: DirectiveMetadata, directives: DirectiveMetadata[]): + Promise { + var parsedTemplate = parser.parse( + domParser.parse(component.template.template, component.type.typeName), directives); + var commands = commandCompiler.compileComponentRuntime(component, parsedTemplate, + componentTemplateFactory); + return PromiseWrapper.resolve(humanize(commands)); + } + + createTests(run); + }); + + + describe('compileComponentCodeGen', () => { + beforeEach(() => { + componentTemplateFactory = (directiveType: TypeMetadata, imports: string[][]) => { + imports.push([TEMPLATE_COMMANDS_MODULE_NAME, 'tcm']); + return `new tcm.CompiledTemplate(${escapeSingleQuoteString(directiveType.typeName)}, [])`; + }; + }); + + function run(component: DirectiveMetadata, directives: DirectiveMetadata[]): + Promise { + var parsedTemplate = parser.parse( + domParser.parse(component.template.template, component.type.typeName), directives); + var sourceModule = commandCompiler.compileComponentCodeGen(component, parsedTemplate, + componentTemplateFactory); + var testableModule = createTestableModule(sourceModule); + return evalModule(testableModule.source, testableModule.imports, null); + } + + createTests(run); + }); + + }); +} + +// Attention: read by eval! +export function humanize(cmds: TemplateCmd[]): any[][] { + var visitor = new CommandHumanizer(); + visitAllCommands(visitor, cmds); + return visitor.result; +} + +function checkAndStringifyType(type: Type): string { + expect(isType(type)).toBe(true); + return `${stringify(type)}Type`; +} + +class CommandHumanizer implements CommandVisitor { + result: any[][] = []; + visitText(cmd: TextCmd, context: any): any { + this.result.push([TEXT, cmd.value, cmd.isBound, cmd.ngContentIndex]); + return null; + } + visitNgContent(cmd: NgContentCmd, context: any): any { + this.result.push([NG_CONTENT, cmd.ngContentIndex]); + return null; + } + visitBeginElement(cmd: BeginElementCmd, context: any): any { + this.result.push([ + BEGIN_ELEMENT, + cmd.name, + cmd.attrNameAndValues, + cmd.eventNames, + cmd.variableNameAndValues, + cmd.directives.map(checkAndStringifyType), + cmd.isBound, + cmd.ngContentIndex + ]); + return null; + } + visitEndElement(context: any): any { + this.result.push([END_ELEMENT]); + return null; + } + visitBeginComponent(cmd: BeginComponentCmd, context: any): any { + this.result.push([ + BEGIN_COMPONENT, + cmd.name, + cmd.attrNameAndValues, + cmd.eventNames, + cmd.variableNameAndValues, + cmd.directives.map(checkAndStringifyType), + cmd.nativeShadow, + cmd.ngContentIndex, + cmd.template.id + ]); + return null; + } + visitEndComponent(context: any): any { + this.result.push([END_COMPONENT]); + return null; + } + visitEmbeddedTemplate(cmd: EmbeddedTemplateCmd, context: any): any { + this.result.push([ + EMBEDDED_TEMPLATE, + cmd.attrNameAndValues, + cmd.variableNameAndValues, + cmd.directives.map(checkAndStringifyType), + cmd.isMerged, + cmd.ngContentIndex, + humanize(cmd.children) + ]); + return null; + } +} + +function createTestableModule(sourceModule: SourceModule): SourceModule { + var testableSource; + var testableImports = [[MODULE_NAME, 'mocks']].concat(sourceModule.imports); + if (IS_DART) { + testableSource = `${sourceModule.source} + run(_) { return mocks.humanize(COMMANDS); }`; + } else { + testableSource = `${sourceModule.source} + exports.run = function(_) { return mocks.humanize(COMMANDS); }`; + } + return new SourceModule(null, testableSource, testableImports); +} diff --git a/modules/angular2/test/compiler/style_compiler_spec.ts b/modules/angular2/test/compiler/style_compiler_spec.ts index 264b41cfc5..55db6131dd 100644 --- a/modules/angular2/test/compiler/style_compiler_spec.ts +++ b/modules/angular2/test/compiler/style_compiler_spec.ts @@ -20,12 +20,8 @@ import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async'; import {evalModule} from './eval_module'; import {StyleCompiler} from 'angular2/src/compiler/style_compiler'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; -import { - DirectiveMetadata, - TemplateMetadata, - TypeMetadata, - ViewEncapsulation -} from 'angular2/src/compiler/api'; +import {DirectiveMetadata, TemplateMetadata, TypeMetadata} from 'angular2/src/compiler/api'; +import {ViewEncapsulation} from 'angular2/src/core/render/api'; // Attention: These module names have to correspond to real modules! const MODULE_NAME = 'angular2/test/compiler/style_compiler_spec'; diff --git a/modules/angular2/test/compiler/template_parser_spec.ts b/modules/angular2/test/compiler/template_parser_spec.ts index 72c463d419..96c0f638f4 100644 --- a/modules/angular2/test/compiler/template_parser_spec.ts +++ b/modules/angular2/test/compiler/template_parser_spec.ts @@ -4,7 +4,12 @@ import {isPresent} from 'angular2/src/core/facade/lang'; import {Parser, Lexer} from 'angular2/src/core/change_detection/change_detection'; import {TemplateParser, splitClasses} from 'angular2/src/compiler/template_parser'; import {HtmlParser} from 'angular2/src/compiler/html_parser'; -import {DirectiveMetadata, TypeMetadata, ChangeDetectionMetadata} from 'angular2/src/compiler/api'; +import { + DirectiveMetadata, + TypeMetadata, + ChangeDetectionMetadata, + TemplateMetadata +} from 'angular2/src/compiler/api'; import { templateVisitAll, TemplateAstVisitor, @@ -62,7 +67,7 @@ export function main() { it('should parse elements with attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [AttrAst, 'a', 'b', 'TestComp > div:nth-child(0)[a=b]'] ]); }); @@ -71,7 +76,7 @@ export function main() { it('should parse ngContent', () => { var parsed = parse('', []); expect(humanizeTemplateAsts(parsed)) - .toEqual([[NgContentAst, 'a', 'TestComp > ng-content:nth-child(0)']]); + .toEqual([[NgContentAst, 'TestComp > ng-content:nth-child(0)']]); }); it('should parse bound text nodes', () => { @@ -84,7 +89,7 @@ export function main() { it('should parse and camel case bound properties', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -99,7 +104,7 @@ export function main() { it('should normalize property names via the element schema', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -114,7 +119,7 @@ export function main() { it('should parse and camel case bound attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Attribute, @@ -129,7 +134,7 @@ export function main() { it('should parse and dash case bound classes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Class, @@ -144,7 +149,7 @@ export function main() { it('should parse and camel case bound styles', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Style, @@ -159,7 +164,7 @@ export function main() { it('should parse bound properties via [...] and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -174,7 +179,7 @@ export function main() { it('should parse bound properties via bind- and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -189,7 +194,7 @@ export function main() { it('should parse bound properties via {{...}} and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -208,7 +213,7 @@ export function main() { it('should parse bound events with a target', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundEventAst, 'event', @@ -222,7 +227,7 @@ export function main() { it('should parse bound events via (...) and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [BoundEventAst, 'event', null, 'v', 'TestComp > div:nth-child(0)[(event)=v]'] ]); }); @@ -230,7 +235,7 @@ export function main() { it('should camel case event names', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundEventAst, 'someEvent', @@ -244,7 +249,7 @@ export function main() { it('should parse bound events via on- and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [BoundEventAst, 'event', null, 'v', 'TestComp > div:nth-child(0)[on-event=v]'] ]); }); @@ -256,7 +261,7 @@ export function main() { () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -279,7 +284,7 @@ export function main() { () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -305,7 +310,7 @@ export function main() { it('should parse variables via #... and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[#a=b]'] ]); }); @@ -313,7 +318,7 @@ export function main() { it('should parse variables via var-... and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[var-a=b]'] ]); }); @@ -321,7 +326,7 @@ export function main() { it('should camel case variables', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [VariableAst, 'someA', 'b', 'TestComp > div:nth-child(0)[var-some-a=b]'] ]); }); @@ -329,7 +334,7 @@ export function main() { it('should use $implicit as variable name if none was specified', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [VariableAst, 'a', '$implicit', 'TestComp > div:nth-child(0)[var-a=]'] ]); }); @@ -341,11 +346,15 @@ export function main() { {selector: '[a=b]', type: new TypeMetadata({typeName: 'DirA'})}); var dirB = new DirectiveMetadata({selector: '[a]', type: new TypeMetadata({typeName: 'DirB'})}); - var comp = new DirectiveMetadata( - {selector: 'div', isComponent: true, type: new TypeMetadata({typeName: 'ZComp'})}); + var comp = new DirectiveMetadata({ + selector: 'div', + isComponent: true, + type: new TypeMetadata({typeName: 'ZComp'}), + template: new TemplateMetadata({ngContentSelectors: []}) + }); expect(humanizeTemplateAsts(parse('
', [dirB, dirA, comp]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [AttrAst, 'a', 'b', 'TestComp > div:nth-child(0)[a=b]'], [DirectiveAst, comp, 'TestComp > div:nth-child(0)'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], @@ -360,7 +369,7 @@ export function main() { new DirectiveMetadata({selector: '[b]', type: new TypeMetadata({typeName: 'DirB'})}); expect(humanizeTemplateAsts(parse('
', [dirA, dirB]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, PropertyBindingType.Property, @@ -380,7 +389,7 @@ export function main() { new DirectiveMetadata({selector: '[b]', type: new TypeMetadata({typeName: 'DirB'})}); expect(humanizeTemplateAsts(parse('
', [dirA, dirB]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[#a=b]'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'] ]); @@ -394,7 +403,7 @@ export function main() { }); expect(humanizeTemplateAsts(parse('
', [dirA]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], [ BoundElementPropertyAst, @@ -415,7 +424,7 @@ export function main() { }); expect(humanizeTemplateAsts(parse('
', [dirA]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], [BoundEventAst, 'a', null, 'expr', 'TestComp > div:nth-child(0)'] ]); @@ -429,7 +438,7 @@ export function main() { }); expect(humanizeTemplateAsts(parse('
', [dirA]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], [ BoundDirectivePropertyAst, @@ -448,7 +457,7 @@ export function main() { }); expect(humanizeTemplateAsts(parse('
', [dirA]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], [BoundDirectivePropertyAst, 'b', 'expr', 'TestComp > div:nth-child(0)[[a]=expr]'] ]); @@ -462,7 +471,7 @@ export function main() { }); expect(humanizeTemplateAsts(parse('
', [dirA]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [AttrAst, 'a', 'literal', 'TestComp > div:nth-child(0)[a=literal]'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], [ @@ -482,7 +491,7 @@ export function main() { }); expect(humanizeTemplateAsts(parse('
', [dirA]))) .toEqual([ - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'] ]); }); @@ -501,7 +510,7 @@ export function main() { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ [EmbeddedTemplateAst, 'TestComp > div:nth-child(0)'], - [ElementAst, 'TestComp > div:nth-child(0)'] + [ElementAst, 'div', 'TestComp > div:nth-child(0)'] ]); }); @@ -516,7 +525,7 @@ export function main() { 'test', 'TestComp > div:nth-child(0)[template=ngIf test]' ], - [ElementAst, 'TestComp > div:nth-child(0)'] + [ElementAst, 'div', 'TestComp > div:nth-child(0)'] ]); }); @@ -525,7 +534,7 @@ export function main() { .toEqual([ [EmbeddedTemplateAst, 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[template=ngIf #a=b]'], - [ElementAst, 'TestComp > div:nth-child(0)'] + [ElementAst, 'div', 'TestComp > div:nth-child(0)'] ]); }); @@ -534,7 +543,7 @@ export function main() { .toEqual([ [EmbeddedTemplateAst, 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[template=ngIf var a=b]'], - [ElementAst, 'TestComp > div:nth-child(0)'] + [ElementAst, 'div', 'TestComp > div:nth-child(0)'] ]); }); @@ -557,7 +566,7 @@ export function main() { 'b', 'TestComp > div:nth-child(0)[template=a b]' ], - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [AttrAst, 'b', '', 'TestComp > div:nth-child(0)[b=]'], [DirectiveAst, dirB, 'TestComp > div:nth-child(0)'] ]); @@ -573,7 +582,7 @@ export function main() { [EmbeddedTemplateAst, 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[template=#a=b]'], [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], - [ElementAst, 'TestComp > div:nth-child(0)'], + [ElementAst, 'div', 'TestComp > div:nth-child(0)'], [AttrAst, 'b', '', 'TestComp > div:nth-child(0)[b=]'], [DirectiveAst, dirB, 'TestComp > div:nth-child(0)'] ]); @@ -592,13 +601,94 @@ export function main() { 'test', 'TestComp > div:nth-child(0)[*ng-if=test]' ], - [ElementAst, 'TestComp > div:nth-child(0)'] + [ElementAst, 'div', 'TestComp > div:nth-child(0)'] ]); }); }); }); + describe('content projection', () => { + function createComp(selector: string, ngContentSelectors: string[]): DirectiveMetadata { + return new DirectiveMetadata({ + selector: selector, + isComponent: true, + type: new TypeMetadata({typeName: 'SomeComp'}), + template: new TemplateMetadata({ngContentSelectors: ngContentSelectors}) + }) + } + + describe('project text nodes', () => { + it('should project text nodes with wildcard selector', () => { + expect(humanizeContentProjection(parse('
hello
', [createComp('div', ['*'])]))) + .toEqual([['div', null], ['#text(hello)', 0]]); + }); + }); + + describe('project elements', () => { + it('should project elements with wildcard selector', () => { + expect(humanizeContentProjection( + parse('
', [createComp('div', ['*'])]))) + .toEqual([['div', null], ['span', 0]]); + }); + + it('should project elements with css selector', () => { + expect(humanizeContentProjection( + parse('
', [createComp('div', ['a[x]'])]))) + .toEqual([['div', null], ['a', 0], ['b', null]]); + }); + }); + + describe('embedded templates', () => { + it('should project embedded templates with wildcard selector', () => { + expect(humanizeContentProjection( + parse('
', [createComp('div', ['*'])]))) + .toEqual([['div', null], ['template', 0]]); + }); + + it('should project embedded templates with css selector', () => { + expect(humanizeContentProjection( + parse('
', + [createComp('div', ['template[x]'])]))) + .toEqual([['div', null], ['template', 0], ['template', null]]); + }); + }); + + describe('ng-content', () => { + it('should project ng-content with wildcard selector', () => { + expect(humanizeContentProjection( + parse('
', [createComp('div', ['*'])]))) + .toEqual([['div', null], ['ng-content', 0]]); + }); + + it('should project ng-content with css selector', () => { + expect(humanizeContentProjection( + parse('
', + [createComp('div', ['ng-content[x]'])]))) + .toEqual([['div', null], ['ng-content', 0], ['ng-content', null]]); + }); + }); + + it('should project into the first matching ng-content', () => { + expect(humanizeContentProjection( + parse('
hello
', [createComp('div', ['a', 'b', '*'])]))) + .toEqual([['div', null], ['#text(hello)', 2], ['b', 1], ['a', 0]]); + }); + + it('should only project direct child nodes', () => { + expect(humanizeContentProjection( + parse('
', [createComp('div', ['a'])]))) + .toEqual([['div', null], ['span', null], ['a', null], ['a', 0]]); + }); + + it('should project nodes of nested components', () => { + expect(humanizeContentProjection( + parse('hello', [createComp('a', ['*']), createComp('b', ['*'])]))) + .toEqual([['a', null], ['b', 0], ['#text(hello)', 0]]); + }); + + }); + describe('splitClasses', () => { it('should keep an empty class', () => { expect(splitClasses('a')).toEqual(['a']); }); @@ -629,18 +719,30 @@ Parser Error: Unexpected token 'b' at column 3 in [a b] in TestComp > div:nth-ch }); it('should not allow more than 1 component per element', () => { - var dirA = new DirectiveMetadata( - {selector: 'div', isComponent: true, type: new TypeMetadata({typeName: 'DirA'})}); - var dirB = new DirectiveMetadata( - {selector: 'div', isComponent: true, type: new TypeMetadata({typeName: 'DirB'})}); + var dirA = new DirectiveMetadata({ + selector: 'div', + isComponent: true, + type: new TypeMetadata({typeName: 'DirA'}), + template: new TemplateMetadata({ngContentSelectors: []}) + }); + var dirB = new DirectiveMetadata({ + selector: 'div', + isComponent: true, + type: new TypeMetadata({typeName: 'DirB'}), + template: new TemplateMetadata({ngContentSelectors: []}) + }); expect(() => parse('
', [dirB, dirA])).toThrowError(`Template parse errors: More than one component: DirA,DirB in TestComp > div:nth-child(0)`); }); it('should not allow components or element nor event bindings on explicit embedded templates', () => { - var dirA = new DirectiveMetadata( - {selector: '[a]', isComponent: true, type: new TypeMetadata({typeName: 'DirA'})}); + var dirA = new DirectiveMetadata({ + selector: '[a]', + isComponent: true, + type: new TypeMetadata({typeName: 'DirA'}), + template: new TemplateMetadata({ngContentSelectors: []}) + }); expect(() => parse('', [dirA])) .toThrowError(`Template parse errors: Components on an embedded template: DirA in TestComp > template:nth-child(0) @@ -649,8 +751,12 @@ Event binding e on an embedded template in TestComp > template:nth-child(0)[(e)= }); it('should not allow components or element bindings on inline embedded templates', () => { - var dirA = new DirectiveMetadata( - {selector: '[a]', isComponent: true, type: new TypeMetadata({typeName: 'DirA'})}); + var dirA = new DirectiveMetadata({ + selector: '[a]', + isComponent: true, + type: new TypeMetadata({typeName: 'DirA'}), + template: new TemplateMetadata({ngContentSelectors: []}) + }); expect(() => parse('
', [dirA])).toThrowError(`Template parse errors: Components on an embedded template: DirA in TestComp > div:nth-child(0) Property binding a not used by any directive on an embedded template in TestComp > div:nth-child(0)[*a=b]`); @@ -668,7 +774,7 @@ export function humanizeTemplateAsts(templateAsts: TemplateAst[]): any[] { class TemplateHumanizer implements TemplateAstVisitor { result: any[] = []; visitNgContent(ast: NgContentAst, context: any): any { - this.result.push([NgContentAst, ast.select, ast.sourceInfo]); + this.result.push([NgContentAst, ast.sourceInfo]); return null; } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { @@ -680,7 +786,7 @@ class TemplateHumanizer implements TemplateAstVisitor { return null; } visitElement(ast: ElementAst, context: any): any { - this.result.push([ElementAst, ast.sourceInfo]); + this.result.push([ElementAst, ast.name, ast.sourceInfo]); templateVisitAll(this, ast.attrs); templateVisitAll(this, ast.properties); templateVisitAll(this, ast.events); @@ -744,6 +850,44 @@ class TemplateHumanizer implements TemplateAstVisitor { } } +function humanizeContentProjection(templateAsts: TemplateAst[]): any[] { + var humanizer = new TemplateContentProjectionHumanizer(); + templateVisitAll(humanizer, templateAsts); + return humanizer.result; +} + +class TemplateContentProjectionHumanizer implements TemplateAstVisitor { + result: any[] = []; + visitNgContent(ast: NgContentAst, context: any): any { + this.result.push(['ng-content', ast.ngContentIndex]); + return null; + } + visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { + this.result.push(['template', ast.ngContentIndex]); + templateVisitAll(this, ast.children); + return null; + } + visitElement(ast: ElementAst, context: any): any { + this.result.push([ast.name, ast.ngContentIndex]); + templateVisitAll(this, ast.children); + return null; + } + visitVariable(ast: VariableAst, context: any): any { return null; } + visitEvent(ast: BoundEventAst, context: any): any { return null; } + visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; } + visitAttr(ast: AttrAst, context: any): any { return null; } + visitBoundText(ast: BoundTextAst, context: any): any { + this.result.push([`#text(${expressionUnparser.unparse(ast.value)})`, ast.ngContentIndex]); + return null; + } + visitText(ast: TextAst, context: any): any { + this.result.push([`#text(${ast.value})`, ast.ngContentIndex]); + return null; + } + visitDirective(ast: DirectiveAst, context: any): any { return null; } + visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; } +} + export class MockSchemaRegistry implements ElementSchemaRegistry { constructor(public existingProperties: StringMap, public attrPropMapping: StringMap) {} diff --git a/modules/angular2/test/compiler/util_spec.ts b/modules/angular2/test/compiler/util_spec.ts new file mode 100644 index 0000000000..693fc69d9b --- /dev/null +++ b/modules/angular2/test/compiler/util_spec.ts @@ -0,0 +1,42 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + el, + expect, + iit, + inject, + it, + xit, + TestComponentBuilder +} from 'angular2/test_lib'; + +import {escapeSingleQuoteString, escapeDoubleQuoteString} from 'angular2/src/compiler/util'; + +export function main() { + describe('util', () => { + describe('escapeSingleQuoteString', () => { + it('should escape single quotes', + () => { expect(escapeSingleQuoteString(`'`)).toEqual(`'\\''`); }); + + it('should escape backslash', + () => { expect(escapeSingleQuoteString('\\')).toEqual(`'\\\\'`); }); + + it('should escape newlines', + () => { expect(escapeSingleQuoteString('\n')).toEqual(`'\\n'`); }); + }); + + describe('escapeDoubleQuoteString', () => { + it('should escape double quotes', + () => { expect(escapeDoubleQuoteString(`"`)).toEqual(`"\\""`); }); + + it('should escape backslash', + () => { expect(escapeDoubleQuoteString('\\')).toEqual(`"\\\\"`); }); + + it('should escape newlines', + () => { expect(escapeDoubleQuoteString('\n')).toEqual(`"\\n"`); }); + }); + + }); +} \ No newline at end of file