From 6d4bd5d9015e85839191065437983d20956611ef Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Wed, 7 Oct 2015 17:15:12 -0700 Subject: [PATCH] fix(render): recurse into components/embedded templates not until all elements in a view have been visited Fixes #4551 Closes #4601 --- .../src/core/compiler/command_compiler.ts | 12 +- .../src/core/compiler/template_ast.ts | 2 +- .../src/core/compiler/template_parser.ts | 5 +- .../src/core/linker/template_commands.ts | 6 +- modules/angular2/src/core/render/api.ts | 8 +- .../angular2/src/core/render/view_factory.ts | 231 +++++++++--------- .../angular2/src/web_workers/shared/api.ts | 2 +- .../src/web_workers/shared/serializer.ts | 4 +- .../linker/projection_integration_spec.ts | 97 ++++++++ .../test/core/render/view_factory_spec.ts | 58 ++++- modules/angular2/test/public_api_spec.ts | 2 + 11 files changed, 294 insertions(+), 133 deletions(-) diff --git a/modules/angular2/src/core/compiler/command_compiler.ts b/modules/angular2/src/core/compiler/command_compiler.ts index 976eab4358..e79e4d6ca6 100644 --- a/modules/angular2/src/core/compiler/command_compiler.ts +++ b/modules/angular2/src/core/compiler/command_compiler.ts @@ -74,7 +74,7 @@ export class CommandCompiler { interface CommandFactory { createText(value: string, isBound: boolean, ngContentIndex: number): R; - createNgContent(ngContentIndex: number): R; + createNgContent(index: number, ngContentIndex: number): R; createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[], variableNameAndValues: string[], directives: CompileDirectiveMetadata[], isBound: boolean, ngContentIndex: number): R; @@ -114,7 +114,9 @@ class RuntimeCommandFactory implements CommandFactory { createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd { return text(value, isBound, ngContentIndex); } - createNgContent(ngContentIndex: number): TemplateCmd { return ngContent(ngContentIndex); } + createNgContent(index: number, ngContentIndex: number): TemplateCmd { + return ngContent(index, ngContentIndex); + } createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[], variableNameAndValues: string[], directives: CompileDirectiveMetadata[], isBound: boolean, ngContentIndex: number): TemplateCmd { @@ -169,8 +171,8 @@ class CodegenCommandFactory implements CommandFactory { createText(value: string, isBound: boolean, ngContentIndex: number): string { return `${TEMPLATE_COMMANDS_MODULE_REF}text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`; } - createNgContent(ngContentIndex: number): string { - return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${ngContentIndex})`; + createNgContent(index: number, ngContentIndex: number): string { + return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${index}, ${ngContentIndex})`; } createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[], variableNameAndValues: string[], directives: CompileDirectiveMetadata[], @@ -221,7 +223,7 @@ class CommandBuilderVisitor implements TemplateAstVisitor { visitNgContent(ast: NgContentAst, context: any): any { this.transitiveNgContentCount++; - this.result.push(this.commandFactory.createNgContent(ast.ngContentIndex)); + this.result.push(this.commandFactory.createNgContent(ast.index, ast.ngContentIndex)); return null; } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { diff --git a/modules/angular2/src/core/compiler/template_ast.ts b/modules/angular2/src/core/compiler/template_ast.ts index 54f1c31033..facd422a5a 100644 --- a/modules/angular2/src/core/compiler/template_ast.ts +++ b/modules/angular2/src/core/compiler/template_ast.ts @@ -104,7 +104,7 @@ export class DirectiveAst implements TemplateAst { } export class NgContentAst implements TemplateAst { - constructor(public ngContentIndex: number, public sourceInfo: string) {} + constructor(public index: number, public ngContentIndex: number, public sourceInfo: string) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitNgContent(this, context); } diff --git a/modules/angular2/src/core/compiler/template_parser.ts b/modules/angular2/src/core/compiler/template_parser.ts index e760b213f4..4331b78ee5 100644 --- a/modules/angular2/src/core/compiler/template_parser.ts +++ b/modules/angular2/src/core/compiler/template_parser.ts @@ -96,6 +96,8 @@ class TemplateParseVisitor implements HtmlAstVisitor { selectorMatcher: SelectorMatcher; errors: string[] = []; directivesIndex = new Map(); + ngContentCount: number = 0; + constructor(directives: CompileDirectiveMetadata[], private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry) { this.selectorMatcher = new SelectorMatcher(); @@ -207,7 +209,8 @@ class TemplateParseVisitor implements HtmlAstVisitor { hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector); var parsedElement; if (preparsedElement.type === PreparsedElementType.NG_CONTENT) { - parsedElement = new NgContentAst(elementNgContentIndex, element.sourceInfo); + parsedElement = + new NgContentAst(this.ngContentCount++, elementNgContentIndex, element.sourceInfo); } else if (isTemplateElement) { this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps, events, element.sourceInfo); diff --git a/modules/angular2/src/core/linker/template_commands.ts b/modules/angular2/src/core/linker/template_commands.ts index 7adea0c832..f9d91d8647 100644 --- a/modules/angular2/src/core/linker/template_commands.ts +++ b/modules/angular2/src/core/linker/template_commands.ts @@ -72,14 +72,14 @@ export function text(value: string, isBound: boolean, ngContentIndex: number): T export class NgContentCmd implements TemplateCmd, RenderNgContentCmd { isBound: boolean = false; - constructor(public ngContentIndex: number) {} + constructor(public index: number, public ngContentIndex: number) {} visit(visitor: RenderCommandVisitor, context: any): any { return visitor.visitNgContent(this, context); } } -export function ngContent(ngContentIndex: number): NgContentCmd { - return new NgContentCmd(ngContentIndex); +export function ngContent(index: number, ngContentIndex: number): NgContentCmd { + return new NgContentCmd(index, ngContentIndex); } export interface IBeginElementCmd extends TemplateCmd, RenderBeginElementCmd { diff --git a/modules/angular2/src/core/render/api.ts b/modules/angular2/src/core/render/api.ts index 2fce9d1aa2..3967bc5e7d 100644 --- a/modules/angular2/src/core/render/api.ts +++ b/modules/angular2/src/core/render/api.ts @@ -85,7 +85,13 @@ export interface RenderBeginCmd extends RenderTemplateCmd { export interface RenderTextCmd extends RenderBeginCmd { value: string; } -export interface RenderNgContentCmd { ngContentIndex: number; } +export interface RenderNgContentCmd { + // The index of this NgContent element + index: number; + // The index of the NgContent element into which this + // NgContent element should be projected (if any) + ngContentIndex: number; +} export interface RenderBeginElementCmd extends RenderBeginCmd { name: string; diff --git a/modules/angular2/src/core/render/view_factory.ts b/modules/angular2/src/core/render/view_factory.ts index a59845d6f7..b66a06487e 100644 --- a/modules/angular2/src/core/render/view_factory.ts +++ b/modules/angular2/src/core/render/view_factory.ts @@ -13,58 +13,20 @@ import {DefaultRenderView, DefaultRenderFragmentRef} from './view'; export function createRenderView(fragmentCmds: RenderTemplateCmd[], inplaceElement: any, nodeFactory: NodeFactory): DefaultRenderView { - var builders: RenderViewBuilder[] = []; - visitAll(new RenderViewBuilder(null, null, inplaceElement, builders, nodeFactory), - fragmentCmds); - var boundElements: any[] = []; - var boundTextNodes: any[] = []; - var nativeShadowRoots: any[] = []; - var fragments: DefaultRenderFragmentRef[] = []; - var viewElementOffset = 0; var view: DefaultRenderView; var eventDispatcher = (boundElementIndex: number, eventName: string, event: any) => view.dispatchRenderEvent(boundElementIndex, eventName, event); - var globalEventAdders: Function[] = []; - - for (var i = 0; i < builders.length; i++) { - var builder = builders[i]; - addAll(builder.boundElements, boundElements); - addAll(builder.boundTextNodes, boundTextNodes); - addAll(builder.nativeShadowRoots, nativeShadowRoots); - if (isBlank(builder.rootNodesParent)) { - fragments.push(new DefaultRenderFragmentRef(builder.fragmentRootNodes)); - } - for (var j = 0; j < builder.eventData.length; j++) { - var eventData = builder.eventData[j]; - var boundElementIndex = eventData[0] + viewElementOffset; - var target = eventData[1]; - var eventName = eventData[2]; - if (isPresent(target)) { - var handler = - createEventHandler(boundElementIndex, `${target}:${eventName}`, eventDispatcher); - globalEventAdders.push(createGlobalEventAdder(target, eventName, handler, nodeFactory)); - } else { - var handler = createEventHandler(boundElementIndex, eventName, eventDispatcher); - nodeFactory.on(boundElements[boundElementIndex], eventName, handler); - } - } - viewElementOffset += builder.boundElements.length; + var context = new BuildContext(eventDispatcher, nodeFactory, inplaceElement); + context.build(fragmentCmds); + var fragments: DefaultRenderFragmentRef[] = []; + for (var i = 0; i < context.fragments.length; i++) { + fragments.push(new DefaultRenderFragmentRef(context.fragments[i])); } - view = new DefaultRenderView(fragments, boundTextNodes, boundElements, nativeShadowRoots, - globalEventAdders); + view = new DefaultRenderView(fragments, context.boundTextNodes, context.boundElements, + context.nativeShadowRoots, context.globalEventAdders); return view; } -function createEventHandler(boundElementIndex: number, eventName: string, - eventDispatcher: Function): Function { - return ($event) => eventDispatcher(boundElementIndex, eventName, $event); -} - -function createGlobalEventAdder(target: string, eventName: string, eventHandler: Function, - nodeFactory: NodeFactory): Function { - return () => nodeFactory.globalOn(target, eventName, eventHandler); -} - export interface NodeFactory { resolveComponentTemplate(templateId: number): RenderTemplateCmd[]; createTemplateAnchor(attrNameAndValues: string[]): N; @@ -77,96 +39,156 @@ export interface NodeFactory { globalOn(target: string, eventName: string, callback: Function): Function; } +class BuildContext { + constructor(private _eventDispatcher: Function, public factory: NodeFactory, + private _inplaceElement: N) {} + private _builders: RenderViewBuilder[] = []; + + globalEventAdders: Function[] = []; + boundElements: N[] = []; + boundTextNodes: N[] = []; + nativeShadowRoots: N[] = []; + fragments: N[][] = []; + + build(fragmentCmds: RenderTemplateCmd[]) { + this.enqueueFragmentBuilder(null, fragmentCmds); + this._build(this._builders[0]); + } + + private _build(builder: RenderViewBuilder) { + this._builders = []; + builder.build(this); + var enqueuedBuilders = this._builders; + for (var i = 0; i < enqueuedBuilders.length; i++) { + this._build(enqueuedBuilders[i]); + } + } + + enqueueComponentBuilder(component: Component) { + this._builders.push(new RenderViewBuilder( + component, null, this.factory.resolveComponentTemplate(component.cmd.templateId))); + } + + enqueueFragmentBuilder(parentComponent: Component, commands: RenderTemplateCmd[]) { + var rootNodes = []; + this.fragments.push(rootNodes); + this._builders.push(new RenderViewBuilder(parentComponent, rootNodes, commands)); + } + + consumeInplaceElement(): N { + var result = this._inplaceElement; + this._inplaceElement = null; + return result; + } + + addEventListener(boundElementIndex: number, target: string, eventName: string) { + if (isPresent(target)) { + var handler = + createEventHandler(boundElementIndex, `${target}:${eventName}`, this._eventDispatcher); + this.globalEventAdders.push(createGlobalEventAdder(target, eventName, handler, this.factory)); + } else { + var handler = createEventHandler(boundElementIndex, eventName, this._eventDispatcher); + this.factory.on(this.boundElements[boundElementIndex], eventName, handler); + } + } +} + + +function createEventHandler(boundElementIndex: number, eventName: string, + eventDispatcher: Function): Function { + return ($event) => eventDispatcher(boundElementIndex, eventName, $event); +} + +function createGlobalEventAdder(target: string, eventName: string, eventHandler: Function, + nodeFactory: NodeFactory): Function { + return () => nodeFactory.globalOn(target, eventName, eventHandler); +} + class RenderViewBuilder implements RenderCommandVisitor { parentStack: Array>; - boundTextNodes: N[] = []; - boundElements: N[] = []; - eventData: any[][] = []; - fragmentRootNodes: N[] = []; - nativeShadowRoots: N[] = []; - - constructor(public parentComponent: Component, public rootNodesParent: N, - public inplaceElement: N, public allBuilders: RenderViewBuilder[], - public factory: NodeFactory) { + constructor(public parentComponent: Component, public fragmentRootNodes: N[], + public commands: RenderTemplateCmd[]) { + var rootNodesParent = isPresent(fragmentRootNodes) ? null : parentComponent.shadowRoot; this.parentStack = [rootNodesParent]; - allBuilders.push(this); + } + + build(context: BuildContext) { + for (var i = 0; i < this.commands.length; i++) { + this.commands[i].visit(this, context); + } } get parent(): N | Component { return this.parentStack[this.parentStack.length - 1]; } - visitText(cmd: RenderTextCmd, context: any): any { - var text = this.factory.createText(cmd.value); - this._addChild(text, cmd.ngContentIndex); + visitText(cmd: RenderTextCmd, context: BuildContext): any { + var text = context.factory.createText(cmd.value); + this._addChild(text, cmd.ngContentIndex, context); if (cmd.isBound) { - this.boundTextNodes.push(text); + context.boundTextNodes.push(text); } return null; } - visitNgContent(cmd: RenderNgContentCmd, context: any): any { + visitNgContent(cmd: RenderNgContentCmd, context: BuildContext): any { if (isPresent(this.parentComponent)) { - var projectedNodes = this.parentComponent.project(); + var projectedNodes = this.parentComponent.project(cmd.index); for (var i = 0; i < projectedNodes.length; i++) { var node = projectedNodes[i]; - this._addChild(node, cmd.ngContentIndex); + this._addChild(node, cmd.ngContentIndex, context); } } return null; } - visitBeginElement(cmd: RenderBeginElementCmd, context: any): any { - this.parentStack.push(this._beginElement(cmd)); + visitBeginElement(cmd: RenderBeginElementCmd, context: BuildContext): any { + this.parentStack.push(this._beginElement(cmd, context)); return null; } - visitEndElement(context: any): any { + visitEndElement(context: BuildContext): any { this._endElement(); return null; } - visitBeginComponent(cmd: RenderBeginComponentCmd, context: any): any { - var el = this._beginElement(cmd); + visitBeginComponent(cmd: RenderBeginComponentCmd, context: BuildContext): any { + var el = this._beginElement(cmd, context); var root = el; if (cmd.nativeShadow) { - root = this.factory.createShadowRoot(el, cmd.templateId); - this.nativeShadowRoots.push(root); + root = context.factory.createShadowRoot(el, cmd.templateId); + context.nativeShadowRoots.push(root); } - var component = new Component(el, root, cmd, this.factory, this.allBuilders); + var component = new Component(el, root, cmd); + context.enqueueComponentBuilder(component); this.parentStack.push(component); return null; } - visitEndComponent(context: any): any { - var c = >this.parent; - c.build(); + visitEndComponent(context: BuildContext): any { this._endElement(); return null; } - visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: any): any { - var el = this.factory.createTemplateAnchor(cmd.attrNameAndValues); - this._addChild(el, cmd.ngContentIndex); - this.boundElements.push(el); + visitEmbeddedTemplate(cmd: RenderEmbeddedTemplateCmd, context: BuildContext): any { + var el = context.factory.createTemplateAnchor(cmd.attrNameAndValues); + this._addChild(el, cmd.ngContentIndex, context); + context.boundElements.push(el); if (cmd.isMerged) { - visitAll( - new RenderViewBuilder(this.parentComponent, null, null, this.allBuilders, this.factory), - cmd.children); + context.enqueueFragmentBuilder(this.parentComponent, cmd.children); } return null; } - private _beginElement(cmd: RenderBeginElementCmd): N { - var el: N; - if (isPresent(this.inplaceElement)) { - el = this.inplaceElement; - this.inplaceElement = null; - this.factory.mergeElement(el, cmd.attrNameAndValues); + private _beginElement(cmd: RenderBeginElementCmd, context: BuildContext): N { + var el: N = context.consumeInplaceElement(); + if (isPresent(el)) { + context.factory.mergeElement(el, cmd.attrNameAndValues); this.fragmentRootNodes.push(el); } else { - el = this.factory.createElement(cmd.name, cmd.attrNameAndValues); - this._addChild(el, cmd.ngContentIndex); + el = context.factory.createElement(cmd.name, cmd.attrNameAndValues); + this._addChild(el, cmd.ngContentIndex, context); } if (cmd.isBound) { - this.boundElements.push(el); + var boundElementIndex = context.boundElements.length; + context.boundElements.push(el); for (var i = 0; i < cmd.eventTargetAndNames.length; i += 2) { var target = cmd.eventTargetAndNames[i]; var eventName = cmd.eventTargetAndNames[i + 1]; - this.eventData.push([this.boundElements.length - 1, target, eventName]); + context.addEventListener(boundElementIndex, target, eventName); } } return el; @@ -174,13 +196,13 @@ class RenderViewBuilder implements RenderCommandVisitor { private _endElement() { this.parentStack.pop(); } - private _addChild(node: N, ngContentIndex: number) { + private _addChild(node: N, ngContentIndex: number, context: BuildContext) { var parent = this.parent; if (isPresent(parent)) { if (parent instanceof Component) { - parent.addContentNode(ngContentIndex, node); + parent.addContentNode(ngContentIndex, node, context); } else { - this.factory.appendChild(parent, node); + context.factory.appendChild(parent, node); } } else { this.fragmentRootNodes.push(node); @@ -190,17 +212,12 @@ class RenderViewBuilder implements RenderCommandVisitor { class Component { private contentNodesByNgContentIndex: N[][] = []; - private projectingNgContentIndex: number = 0; - private viewBuilder: RenderViewBuilder; - constructor(public hostElement: N, shadowRoot: N, public cmd: RenderBeginComponentCmd, - public factory: NodeFactory, allBuilders: RenderViewBuilder[]) { - this.viewBuilder = new RenderViewBuilder(this, shadowRoot, null, allBuilders, factory); - } - addContentNode(ngContentIndex: number, node: N) { + constructor(public hostElement: N, public shadowRoot: N, public cmd: RenderBeginComponentCmd) {} + addContentNode(ngContentIndex: number, node: N, context: BuildContext) { if (isBlank(ngContentIndex)) { if (this.cmd.nativeShadow) { - this.factory.appendChild(this.hostElement, node); + context.factory.appendChild(this.hostElement, node); } } else { while (this.contentNodesByNgContentIndex.length <= ngContentIndex) { @@ -209,15 +226,11 @@ class Component { this.contentNodesByNgContentIndex[ngContentIndex].push(node); } } - project(): N[] { - var ngContentIndex = this.projectingNgContentIndex++; + project(ngContentIndex: number): N[] { return ngContentIndex < this.contentNodesByNgContentIndex.length ? this.contentNodesByNgContentIndex[ngContentIndex] : []; } - build() { - visitAll(this.viewBuilder, this.factory.resolveComponentTemplate(this.cmd.templateId)); - } } function addAll(source: any[], target: any[]) { @@ -225,9 +238,3 @@ function addAll(source: any[], target: any[]) { target.push(source[i]); } } - -function visitAll(visitor: RenderCommandVisitor, fragmentCmds: RenderTemplateCmd[]) { - for (var i = 0; i < fragmentCmds.length; i++) { - fragmentCmds[i].visit(visitor, null); - } -} \ No newline at end of file diff --git a/modules/angular2/src/web_workers/shared/api.ts b/modules/angular2/src/web_workers/shared/api.ts index 2808f3cc4b..f916e5d6e9 100644 --- a/modules/angular2/src/web_workers/shared/api.ts +++ b/modules/angular2/src/web_workers/shared/api.ts @@ -30,7 +30,7 @@ export class WebWorkerTextCmd implements RenderTextCmd { } export class WebWorkerNgContentCmd implements RenderNgContentCmd { - constructor(public ngContentIndex: number) {} + constructor(public index: number, public ngContentIndex: number) {} visit(visitor: RenderCommandVisitor, context: any): any { return visitor.visitNgContent(this, context); } diff --git a/modules/angular2/src/web_workers/shared/serializer.ts b/modules/angular2/src/web_workers/shared/serializer.ts index bd8f9bfad6..c9118de6bb 100644 --- a/modules/angular2/src/web_workers/shared/serializer.ts +++ b/modules/angular2/src/web_workers/shared/serializer.ts @@ -163,7 +163,7 @@ class RenderTemplateCmdSerializer implements RenderCommandVisitor { }; } visitNgContent(cmd: RenderNgContentCmd, context: any): any { - return {'deserializerIndex': 1, 'ngContentIndex': cmd.ngContentIndex}; + return {'deserializerIndex': 1, 'index': cmd.index, 'ngContentIndex': cmd.ngContentIndex}; } visitBeginElement(cmd: RenderBeginElementCmd, context: any): any { return { @@ -209,7 +209,7 @@ var RENDER_TEMPLATE_CMD_SERIALIZER = new RenderTemplateCmdSerializer(); var RENDER_TEMPLATE_CMD_DESERIALIZERS = [ (data: {[key: string]: any}) => new WebWorkerTextCmd(data['isBound'], data['ngContentIndex'], data['value']), - (data: {[key: string]: any}) => new WebWorkerNgContentCmd(data['ngContentIndex']), + (data: {[key: string]: any}) => new WebWorkerNgContentCmd(data['index'], data['ngContentIndex']), (data: {[key: string]: any}) => new WebWorkerBeginElementCmd(data['isBound'], data['ngContentIndex'], data['name'], data['attrNameAndValues'], data['eventTargetAndNames']), diff --git a/modules/angular2/test/core/linker/projection_integration_spec.ts b/modules/angular2/test/core/linker/projection_integration_spec.ts index e293352924..58f71f5f06 100644 --- a/modules/angular2/test/core/linker/projection_integration_spec.ts +++ b/modules/angular2/test/core/linker/projection_integration_spec.ts @@ -21,6 +21,7 @@ import { } from 'angular2/test_lib'; import {DOM} from 'angular2/src/core/dom/dom_adapter'; +import {AppViewListener} from 'angular2/src/core/linker/view_listener'; import { bind, @@ -38,6 +39,8 @@ import {By} from 'angular2/src/core/debug'; export function main() { describe('projection', () => { + beforeEachBindings(() => [bind(AppViewListener).toClass(AppViewListener)]); + it('should support simple components', inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { tcb.overrideView(MainComp, new ViewMetadata({ @@ -464,6 +467,38 @@ export function main() { }); })); + it('should allow to switch the order of nested components via ng-content', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView( + MainComp, + new ViewMetadata( + {template: ``, directives: [CmpA, CmpB]})) + .createAsync(MainComp) + .then((main) => { + main.detectChanges(); + expect(DOM.getInnerHTML(main.debugElement.nativeElement)) + .toEqual( + 'cmp-dcmp-c'); + async.done(); + }); + })); + + it('should create nested components in the right order', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + tcb.overrideView( + MainComp, + new ViewMetadata( + {template: ``, directives: [CmpA1, CmpA2]})) + .createAsync(MainComp) + .then((main) => { + main.detectChanges(); + expect(DOM.getInnerHTML(main.debugElement.nativeElement)) + .toEqual( + 'a1b11b12a2b21b22'); + async.done(); + }); + })); + }); } @@ -600,3 +635,65 @@ class Tab { class Tree { depth = 0; } + + +@Component({selector: 'cmp-d'}) +@View({template: `{{tagName}}`}) +class CmpD { + tagName: string; + constructor(elementRef: ElementRef) { + this.tagName = DOM.tagName(elementRef.nativeElement).toLowerCase(); + } +} + + +@Component({selector: 'cmp-c'}) +@View({template: `{{tagName}}`}) +class CmpC { + tagName: string; + constructor(elementRef: ElementRef) { + this.tagName = DOM.tagName(elementRef.nativeElement).toLowerCase(); + } +} + + +@Component({selector: 'cmp-b'}) +@View({template: ``, directives: [CmpD]}) +class CmpB { +} + + +@Component({selector: 'cmp-a'}) +@View({template: ``, directives: [CmpC]}) +class CmpA { +} + +@Component({selector: 'cmp-b11'}) +@View({template: `{{'b11'}}`, directives: []}) +class CmpB11 { +} + +@Component({selector: 'cmp-b12'}) +@View({template: `{{'b12'}}`, directives: []}) +class CmpB12 { +} + +@Component({selector: 'cmp-b21'}) +@View({template: `{{'b21'}}`, directives: []}) +class CmpB21 { +} + +@Component({selector: 'cmp-b22'}) +@View({template: `{{'b22'}}`, directives: []}) +class CmpB22 { +} + +@Component({selector: 'cmp-a1'}) +@View({template: `{{'a1'}}`, directives: [CmpB11, CmpB12]}) +class CmpA1 { +} + +@Component({selector: 'cmp-a2'}) +@View({template: `{{'a2'}}`, directives: [CmpB21, CmpB22]}) +class CmpA2 { +} diff --git a/modules/angular2/test/core/render/view_factory_spec.ts b/modules/angular2/test/core/render/view_factory_spec.ts index 0c18ac5c73..3a7ac77cda 100644 --- a/modules/angular2/test/core/render/view_factory_spec.ts +++ b/modules/angular2/test/core/render/view_factory_spec.ts @@ -49,8 +49,8 @@ function endComponent() { return appCmds.endComponent(); } -function ngContent(ngContentIndex: number) { - return appCmds.ngContent(ngContentIndex); +function ngContent(index: number, ngContentIndex: number) { + return appCmds.ngContent(index, ngContentIndex); } export function main() { @@ -369,6 +369,50 @@ export function main() { expect(mapAttrs(view.boundElements, 'id')).toEqual(['1.1', '1.2', '2.1', '3.1']); }); + it('should process nested components in depth first order', () => { + componentTemplates.set(0, [ + beginComponent('b11-comp', ['id', '2.1'], [], false, null, 2), + endComponent(), + beginComponent('b12-comp', ['id', '2.2'], [], false, null, 3), + endComponent(), + ]); + componentTemplates.set(1, [ + beginComponent('b21-comp', ['id', '3.1'], [], false, null, 4), + endComponent(), + beginComponent('b22-comp', ['id', '3.2'], [], false, null, 5), + endComponent(), + ]); + componentTemplates.set(2, [ + beginElement('b11', ['id', '4.11'], [], true, null), + endElement(), + ]); + componentTemplates.set(3, [ + beginElement('b12', ['id', '4.12'], [], true, null), + endElement(), + ]); + componentTemplates.set(4, [ + beginElement('b21', ['id', '4.21'], [], true, null), + endElement(), + ]); + componentTemplates.set(5, [ + beginElement('b22', ['id', '4.22'], [], true, null), + endElement(), + ]); + + var view = createRenderView( + [ + beginComponent('a1-comp', ['id', '1.1'], [], false, null, 0), + endComponent(), + beginComponent('a2-comp', ['id', '1.2'], [], false, null, 1), + endComponent(), + ], + null, nodeFactory); + + expect(mapAttrs(view.boundElements, 'id')) + .toEqual(['1.1', '1.2', '2.1', '2.2', '4.11', '4.12', '3.1', '3.2', '4.21', '4.22']); + }); + + it('should store bound text nodes after the bound text nodes of the main template', () => { componentTemplates.set(0, [ text('2.1', true, null), @@ -442,9 +486,9 @@ export function main() { it('should project commands based on their ngContentIndex', () => { componentTemplates.set(0, [ text('(', false, null), - ngContent(null), + ngContent(0, null), text(',', false, null), - ngContent(null), + ngContent(1, null), text(')', false, null) ]); var view = createRenderView( @@ -460,9 +504,9 @@ export function main() { it('should reproject nodes over multiple ng-content commands', () => { componentTemplates.set( - 0, [beginComponent('b-comp', [], [], false, null, 1), ngContent(0), endComponent()]); - componentTemplates.set(1, - [text('(', false, null), ngContent(null), text(')', false, null)]); + 0, [beginComponent('b-comp', [], [], false, null, 1), ngContent(0, 0), endComponent()]); + componentTemplates.set( + 1, [text('(', false, null), ngContent(0, null), text(')', false, null)]); var view = createRenderView( [ beginComponent('a-comp', [], [], false, null, 0), diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index dcc7f88b8f..4db587abe6 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -1106,6 +1106,8 @@ var NG_API = [ '{RenderEmbeddedTemplateCmd}.isMerged', '{RenderEmbeddedTemplateCmd}.isMerged=', '{RenderNgContentCmd}', + '{RenderNgContentCmd}.index', + '{RenderNgContentCmd}.index=', '{RenderNgContentCmd}.ngContentIndex', '{RenderNgContentCmd}.ngContentIndex=', '{RenderTemplateCmd}',