feat(Compiler): add support for `<ng-container>`
`<ng-container>` is a logical container that can be used to group nodes but is not rendered in the DOM tree as a node. `<ng-container>` is rendered as an HTML comment.
This commit is contained in:
parent
22916bb5d1
commit
0dbff55bc6
|
@ -21,6 +21,7 @@ import {AnimationCompiler} from '../animation/animation_compiler';
|
||||||
const IMPLICIT_TEMPLATE_VAR = '\$implicit';
|
const IMPLICIT_TEMPLATE_VAR = '\$implicit';
|
||||||
const CLASS_ATTR = 'class';
|
const CLASS_ATTR = 'class';
|
||||||
const STYLE_ATTR = 'style';
|
const STYLE_ATTR = 'style';
|
||||||
|
const NG_CONTAINER_TAG = 'ng-container';
|
||||||
|
|
||||||
var parentRenderNodeVar = o.variable('parentRenderNode');
|
var parentRenderNodeVar = o.variable('parentRenderNode');
|
||||||
var rootSelectorVar = o.variable('rootSelector');
|
var rootSelectorVar = o.variable('rootSelector');
|
||||||
|
@ -60,8 +61,10 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
|
|
||||||
private _isRootNode(parent: CompileElement): boolean { return parent.view !== this.view; }
|
private _isRootNode(parent: CompileElement): boolean { return parent.view !== this.view; }
|
||||||
|
|
||||||
private _addRootNodeAndProject(
|
private _addRootNodeAndProject(node: CompileNode) {
|
||||||
node: CompileNode, ngContentIndex: number, parent: CompileElement) {
|
var projectedNode = _getOuterContainerOrSelf(node);
|
||||||
|
var parent = projectedNode.parent;
|
||||||
|
var ngContentIndex = (<any>projectedNode.sourceAst).ngContentIndex;
|
||||||
var vcAppEl =
|
var vcAppEl =
|
||||||
(node instanceof CompileElement && node.hasViewContainer) ? node.appElement : null;
|
(node instanceof CompileElement && node.hasViewContainer) ? node.appElement : null;
|
||||||
if (this._isRootNode(parent)) {
|
if (this._isRootNode(parent)) {
|
||||||
|
@ -75,6 +78,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getParentRenderNode(parent: CompileElement): o.Expression {
|
private _getParentRenderNode(parent: CompileElement): o.Expression {
|
||||||
|
parent = <CompileElement>_getOuterContainerParentOrSelf(parent);
|
||||||
if (this._isRootNode(parent)) {
|
if (this._isRootNode(parent)) {
|
||||||
if (this.view.viewType === ViewType.COMPONENT) {
|
if (this.view.viewType === ViewType.COMPONENT) {
|
||||||
return parentRenderNodeVar;
|
return parentRenderNodeVar;
|
||||||
|
@ -91,14 +95,12 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
|
visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
|
||||||
return this._visitText(ast, '', ast.ngContentIndex, parent);
|
return this._visitText(ast, '', parent);
|
||||||
}
|
}
|
||||||
visitText(ast: TextAst, parent: CompileElement): any {
|
visitText(ast: TextAst, parent: CompileElement): any {
|
||||||
return this._visitText(ast, ast.value, ast.ngContentIndex, parent);
|
return this._visitText(ast, ast.value, parent);
|
||||||
}
|
}
|
||||||
private _visitText(
|
private _visitText(ast: TemplateAst, value: string, parent: CompileElement): o.Expression {
|
||||||
ast: TemplateAst, value: string, ngContentIndex: number,
|
|
||||||
parent: CompileElement): o.Expression {
|
|
||||||
var fieldName = `_text_${this.view.nodes.length}`;
|
var fieldName = `_text_${this.view.nodes.length}`;
|
||||||
this.view.fields.push(
|
this.view.fields.push(
|
||||||
new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderText)));
|
new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderText)));
|
||||||
|
@ -115,7 +117,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
.toStmt();
|
.toStmt();
|
||||||
this.view.nodes.push(compileNode);
|
this.view.nodes.push(compileNode);
|
||||||
this.view.createMethod.addStmt(createRenderNode);
|
this.view.createMethod.addStmt(createRenderNode);
|
||||||
this._addRootNodeAndProject(compileNode, ngContentIndex, parent);
|
this._addRootNodeAndProject(compileNode);
|
||||||
return renderNode;
|
return renderNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,11 +159,16 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
if (nodeIndex === 0 && this.view.viewType === ViewType.HOST) {
|
if (nodeIndex === 0 && this.view.viewType === ViewType.HOST) {
|
||||||
createRenderNodeExpr = o.THIS_EXPR.callMethod(
|
createRenderNodeExpr = o.THIS_EXPR.callMethod(
|
||||||
'selectOrCreateHostElement', [o.literal(ast.name), rootSelectorVar, debugContextExpr]);
|
'selectOrCreateHostElement', [o.literal(ast.name), rootSelectorVar, debugContextExpr]);
|
||||||
|
} else {
|
||||||
|
if (ast.name === NG_CONTAINER_TAG) {
|
||||||
|
createRenderNodeExpr = ViewProperties.renderer.callMethod(
|
||||||
|
'createTemplateAnchor', [this._getParentRenderNode(parent), debugContextExpr]);
|
||||||
} else {
|
} else {
|
||||||
createRenderNodeExpr = ViewProperties.renderer.callMethod(
|
createRenderNodeExpr = ViewProperties.renderer.callMethod(
|
||||||
'createElement',
|
'createElement',
|
||||||
[this._getParentRenderNode(parent), o.literal(ast.name), debugContextExpr]);
|
[this._getParentRenderNode(parent), o.literal(ast.name), debugContextExpr]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var fieldName = `_el_${nodeIndex}`;
|
var fieldName = `_el_${nodeIndex}`;
|
||||||
this.view.fields.push(
|
this.view.fields.push(
|
||||||
new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderElement)));
|
new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderElement)));
|
||||||
|
@ -201,7 +208,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
.toDeclStmt());
|
.toDeclStmt());
|
||||||
}
|
}
|
||||||
compileElement.beforeChildren();
|
compileElement.beforeChildren();
|
||||||
this._addRootNodeAndProject(compileElement, ast.ngContentIndex, parent);
|
this._addRootNodeAndProject(compileElement);
|
||||||
templateVisitAll(this, ast.children, compileElement);
|
templateVisitAll(this, ast.children, compileElement);
|
||||||
compileElement.afterChildren(this.view.nodes.length - nodeIndex - 1);
|
compileElement.afterChildren(this.view.nodes.length - nodeIndex - 1);
|
||||||
|
|
||||||
|
@ -257,7 +264,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies);
|
this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies);
|
||||||
|
|
||||||
compileElement.beforeChildren();
|
compileElement.beforeChildren();
|
||||||
this._addRootNodeAndProject(compileElement, ast.ngContentIndex, parent);
|
this._addRootNodeAndProject(compileElement);
|
||||||
compileElement.afterChildren(0);
|
compileElement.afterChildren(0);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -275,6 +282,46 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||||
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
|
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() && (<ElementAst>node.sourceAst).name === NG_CONTAINER_TAG &&
|
||||||
|
node.view === view;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function _mergeHtmlAndDirectiveAttrs(
|
function _mergeHtmlAndDirectiveAttrs(
|
||||||
declaredHtmlAttrs: {[key: string]: string},
|
declaredHtmlAttrs: {[key: string]: string},
|
||||||
directives: CompileDirectiveMetadata[]): string[][] {
|
directives: CompileDirectiveMetadata[]): string[][] {
|
||||||
|
|
|
@ -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('<ng-container>', function() {
|
||||||
|
|
||||||
|
beforeEachProviders(
|
||||||
|
() =>
|
||||||
|
[{
|
||||||
|
provide: CompilerConfig,
|
||||||
|
useValue: new CompilerConfig({genDebugInfo: true, useJit: useJit})
|
||||||
|
},
|
||||||
|
{provide: ANCHOR_ELEMENT, useValue: el('<div></div>')},
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('should be rendered as comment with children as siblings',
|
||||||
|
inject(
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter],
|
||||||
|
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||||
|
tcb.overrideTemplate(MyComp, '<ng-container><p></p></ng-container>')
|
||||||
|
.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, '<ng-container *ngIf="ctxBoolProp"><p></p><b></b></ng-container>')
|
||||||
|
.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, `<simple><ng-container><p>1</p><p>2</p></ng-container></simple>`)
|
||||||
|
.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, `<ng-container [text]="'container'"><p></p></ng-container>`)
|
||||||
|
.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 <ng-container> (content dom)',
|
||||||
|
inject(
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter],
|
||||||
|
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||||
|
const template =
|
||||||
|
'<needs-content-children #q><ng-container><div text="foo"></div></ng-container></needs-content-children>';
|
||||||
|
|
||||||
|
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 <ng-container> (view dom)',
|
||||||
|
inject(
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter],
|
||||||
|
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||||
|
const template = '<needs-view-children #q></needs-view-children>';
|
||||||
|
|
||||||
|
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<TextDirective>;
|
||||||
|
numberOfChildrenAfterContentInit: number;
|
||||||
|
|
||||||
|
ngAfterContentInit() { this.numberOfChildrenAfterContentInit = this.textDirChildren.length; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component(
|
||||||
|
{selector: 'needs-view-children', template: '<div text></div>', directives: [TextDirective]})
|
||||||
|
class NeedsViewChildren implements AfterViewInit {
|
||||||
|
@ViewChildren(TextDirective) textDirChildren: QueryList<TextDirective>;
|
||||||
|
numberOfChildrenAfterViewInit: number;
|
||||||
|
|
||||||
|
ngAfterViewInit() { this.numberOfChildrenAfterViewInit = this.textDirChildren.length; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'simple', template: 'SIMPLE(<ng-content></ng-content>)', directives: []})
|
||||||
|
class Simple {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-comp',
|
||||||
|
directives: [NeedsContentChildren, NeedsViewChildren, TextDirective, NgIf, Simple],
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
class MyComp {
|
||||||
|
ctxBoolProp: boolean = false;
|
||||||
|
}
|
Loading…
Reference in New Issue