diff --git a/modules/@angular/compiler/src/view_compiler/view_builder.ts b/modules/@angular/compiler/src/view_compiler/view_builder.ts index 2b222b888d..5af0a135a9 100644 --- a/modules/@angular/compiler/src/view_compiler/view_builder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_builder.ts @@ -21,6 +21,7 @@ import {AnimationCompiler} from '../animation/animation_compiler'; const IMPLICIT_TEMPLATE_VAR = '\$implicit'; const CLASS_ATTR = 'class'; const STYLE_ATTR = 'style'; +const NG_CONTAINER_TAG = 'ng-container'; var parentRenderNodeVar = o.variable('parentRenderNode'); var rootSelectorVar = o.variable('rootSelector'); @@ -60,8 +61,10 @@ class ViewBuilderVisitor implements TemplateAstVisitor { private _isRootNode(parent: CompileElement): boolean { return parent.view !== this.view; } - private _addRootNodeAndProject( - node: CompileNode, ngContentIndex: number, parent: CompileElement) { + private _addRootNodeAndProject(node: CompileNode) { + var projectedNode = _getOuterContainerOrSelf(node); + var parent = projectedNode.parent; + var ngContentIndex = (projectedNode.sourceAst).ngContentIndex; var vcAppEl = (node instanceof CompileElement && node.hasViewContainer) ? node.appElement : null; if (this._isRootNode(parent)) { @@ -75,6 +78,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor { } private _getParentRenderNode(parent: CompileElement): o.Expression { + parent = _getOuterContainerParentOrSelf(parent); if (this._isRootNode(parent)) { if (this.view.viewType === ViewType.COMPONENT) { return parentRenderNodeVar; @@ -91,14 +95,12 @@ class ViewBuilderVisitor implements TemplateAstVisitor { } visitBoundText(ast: BoundTextAst, parent: CompileElement): any { - return this._visitText(ast, '', ast.ngContentIndex, parent); + return this._visitText(ast, '', parent); } visitText(ast: TextAst, parent: CompileElement): any { - return this._visitText(ast, ast.value, ast.ngContentIndex, parent); + return this._visitText(ast, ast.value, parent); } - private _visitText( - ast: TemplateAst, value: string, ngContentIndex: number, - parent: CompileElement): o.Expression { + private _visitText(ast: TemplateAst, value: string, parent: CompileElement): o.Expression { var fieldName = `_text_${this.view.nodes.length}`; this.view.fields.push( new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderText))); @@ -115,7 +117,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor { .toStmt(); this.view.nodes.push(compileNode); this.view.createMethod.addStmt(createRenderNode); - this._addRootNodeAndProject(compileNode, ngContentIndex, parent); + this._addRootNodeAndProject(compileNode); return renderNode; } @@ -158,9 +160,14 @@ class ViewBuilderVisitor implements TemplateAstVisitor { createRenderNodeExpr = o.THIS_EXPR.callMethod( 'selectOrCreateHostElement', [o.literal(ast.name), rootSelectorVar, debugContextExpr]); } else { - createRenderNodeExpr = ViewProperties.renderer.callMethod( - 'createElement', - [this._getParentRenderNode(parent), o.literal(ast.name), debugContextExpr]); + if (ast.name === NG_CONTAINER_TAG) { + createRenderNodeExpr = ViewProperties.renderer.callMethod( + 'createTemplateAnchor', [this._getParentRenderNode(parent), debugContextExpr]); + } else { + createRenderNodeExpr = ViewProperties.renderer.callMethod( + 'createElement', + [this._getParentRenderNode(parent), o.literal(ast.name), debugContextExpr]); + } } var fieldName = `_el_${nodeIndex}`; this.view.fields.push( @@ -201,7 +208,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor { .toDeclStmt()); } compileElement.beforeChildren(); - this._addRootNodeAndProject(compileElement, ast.ngContentIndex, parent); + this._addRootNodeAndProject(compileElement); templateVisitAll(this, ast.children, compileElement); compileElement.afterChildren(this.view.nodes.length - nodeIndex - 1); @@ -257,7 +264,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor { this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies); compileElement.beforeChildren(); - this._addRootNodeAndProject(compileElement, ast.ngContentIndex, parent); + this._addRootNodeAndProject(compileElement); compileElement.afterChildren(0); return null; @@ -275,6 +282,46 @@ class ViewBuilderVisitor implements TemplateAstVisitor { visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; } } +/** + * Walks up the nodes while the direct parent is a container. + * + * Returns the outer container or the node itself when it is not a direct child of a container. + * + * @internal + */ +function _getOuterContainerOrSelf(node: CompileNode): CompileNode { + const view = node.view; + + while (_isNgContainer(node.parent, view)) { + node = node.parent; + } + + return node; +} + +/** + * Walks up the nodes while they are container and returns the first parent which is not. + * + * Returns the parent of the outer container or the node itself when it is not a container. + * + * @internal + */ +function _getOuterContainerParentOrSelf(el: CompileElement): CompileElement { + const view = el.view; + + while (_isNgContainer(el, view)) { + el = el.parent; + } + + return el; +} + +function _isNgContainer(node: CompileNode, view: CompileView): boolean { + return !node.isNull() && (node.sourceAst).name === NG_CONTAINER_TAG && + node.view === view; +} + + function _mergeHtmlAndDirectiveAttrs( declaredHtmlAttrs: {[key: string]: string}, directives: CompileDirectiveMetadata[]): string[][] { diff --git a/modules/@angular/core/test/linker/ng_container_integration_spec.ts b/modules/@angular/core/test/linker/ng_container_integration_spec.ts new file mode 100644 index 0000000000..2961ead31e --- /dev/null +++ b/modules/@angular/core/test/linker/ng_container_integration_spec.ts @@ -0,0 +1,188 @@ +import {beforeEach, ddescribe, xdescribe, describe, expect, iit, inject, beforeEachProviders, it, xit,} from '@angular/core/testing/testing_internal'; +import {TestComponentBuilder} from '@angular/compiler/testing'; +import {AsyncTestCompleter} from '@angular/core/testing/testing_internal'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; +import {OpaqueToken, ViewMetadata, Component, Directive, AfterContentInit, AfterViewInit, QueryList, ContentChildren, ViewChildren, Input} from '@angular/core'; +import {NgIf} from '@angular/common'; +import {CompilerConfig} from '@angular/compiler'; +import {el} from '@angular/platform-browser/testing'; + +const ANCHOR_ELEMENT = new OpaqueToken('AnchorElement'); + +export function main() { + describe('jit', () => { declareTests({useJit: true}); }); + describe('no jit', () => { declareTests({useJit: false}); }); +} + +function declareTests({useJit}: {useJit: boolean}) { + describe('', function() { + + beforeEachProviders( + () => + [{ + provide: CompilerConfig, + useValue: new CompilerConfig({genDebugInfo: true, useJit: useJit}) + }, + {provide: ANCHOR_ELEMENT, useValue: el('
')}, + ]); + + it('should be rendered as comment with children as siblings', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + tcb.overrideTemplate(MyComp, '

') + .createAsync(MyComp) + .then((fixture) => { + + fixture.detectChanges(); + + const el = fixture.debugElement.nativeElement; + const children = getDOM().childNodes(el); + expect(children.length).toBe(2); + expect(getDOM().isCommentNode(children[0])).toBe(true); + expect(getDOM().tagName(children[1]).toUpperCase()).toEqual('P'); + + async.done(); + }); + })); + + it('should group inner nodes', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + tcb.overrideTemplate( + MyComp, '

') + .createAsync(MyComp) + .then((fixture) => { + fixture.debugElement.componentInstance.ctxBoolProp = true; + fixture.detectChanges(); + + const el = fixture.debugElement.nativeElement; + const children = getDOM().childNodes(el); + + expect(children.length).toBe(4); + // ngIf anchor + expect(getDOM().isCommentNode(children[0])).toBe(true); + // ng-container anchor + expect(getDOM().isCommentNode(children[1])).toBe(true); + expect(getDOM().tagName(children[2]).toUpperCase()).toEqual('P'); + expect(getDOM().tagName(children[3]).toUpperCase()).toEqual('B'); + + fixture.debugElement.componentInstance.ctxBoolProp = false; + fixture.detectChanges(); + + expect(children.length).toBe(1); + expect(getDOM().isCommentNode(children[0])).toBe(true); + + async.done(); + }); + })); + + it('should work with static content projection', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + tcb.overrideTemplate( + MyComp, `

1

2

`) + .createAsync(MyComp) + .then((fixture) => { + fixture.detectChanges(); + const el = fixture.debugElement.nativeElement; + expect(el).toHaveText('SIMPLE(12)'); + async.done(); + }); + })); + + it('should support injecting the container from children', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + tcb.overrideTemplate( + MyComp, `

`) + .createAsync(MyComp) + .then((fixture) => { + fixture.detectChanges(); + const dir = fixture.debugElement.children[0].injector.get(TextDirective); + expect(dir).toBeAnInstanceOf(TextDirective); + expect(dir.text).toEqual('container'); + async.done(); + }); + })); + + it('should contain all direct child directives in a (content dom)', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const template = + '
'; + + tcb.overrideTemplate(MyComp, template).createAsync(MyComp).then((view) => { + view.detectChanges(); + + var q = view.debugElement.children[0].references['q']; + + view.detectChanges(); + + expect(q.textDirChildren.length).toEqual(1); + expect(q.numberOfChildrenAfterContentInit).toEqual(1); + + async.done(); + }); + })); + + it('should contain all child directives in a (view dom)', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const template = ''; + + tcb.overrideTemplate(MyComp, template).createAsync(MyComp).then((view) => { + view.detectChanges(); + + var q = view.debugElement.children[0].references['q']; + + view.detectChanges(); + + expect(q.textDirChildren.length).toEqual(1); + expect(q.numberOfChildrenAfterViewInit).toEqual(1); + + async.done(); + }); + })); + }); +} + +@Directive({selector: '[text]'}) +class TextDirective { + @Input() public text: string = null; +} + +@Component({selector: 'needs-content-children', template: ''}) +class NeedsContentChildren implements AfterContentInit { + @ContentChildren(TextDirective) textDirChildren: QueryList; + numberOfChildrenAfterContentInit: number; + + ngAfterContentInit() { this.numberOfChildrenAfterContentInit = this.textDirChildren.length; } +} + +@Component( + {selector: 'needs-view-children', template: '
', directives: [TextDirective]}) +class NeedsViewChildren implements AfterViewInit { + @ViewChildren(TextDirective) textDirChildren: QueryList; + numberOfChildrenAfterViewInit: number; + + ngAfterViewInit() { this.numberOfChildrenAfterViewInit = this.textDirChildren.length; } +} + +@Component({selector: 'simple', template: 'SIMPLE()', directives: []}) +class Simple { +} + +@Component({ + selector: 'my-comp', + directives: [NeedsContentChildren, NeedsViewChildren, TextDirective, NgIf, Simple], + template: '' +}) +class MyComp { + ctxBoolProp: boolean = false; +} \ No newline at end of file