Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix(ivy): descend into view containers on ng-template when collecting…
… rootNodes (#33493)

PR Close #33493
  • Loading branch information
pkozlowski-opensource authored and AndrewKushnir committed Oct 30, 2019
1 parent f197191 commit 502fb7e
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 60 deletions.
38 changes: 26 additions & 12 deletions packages/core/src/render3/view_ref.ts
Expand Up @@ -10,10 +10,13 @@ import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
import {assertDefined} from '../util/assert';

import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions/shared';
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
import {CONTEXT, FLAGS, HOST, LView, LViewFlags, T_HOST} from './interfaces/view';
import {CONTEXT, FLAGS, HOST, LView, LViewFlags, TView, T_HOST} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert';
import {destroyLView, renderDetachView} from './node_manipulation';
import {findComponentView, getLViewParent} from './util/view_traversal_utils';
import {getNativeByTNode, getNativeByTNodeOrNull} from './util/view_utils';
Expand All @@ -38,7 +41,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
get rootNodes(): any[] {
if (this._lView[HOST] == null) {
const tView = this._lView[T_HOST] as TViewNode;
return collectNativeNodes(this._lView, tView, []);
return collectNativeNodes(this._lView, tView.child, []);
}
return [];
}
Expand Down Expand Up @@ -299,27 +302,38 @@ export class RootViewRef<T> extends ViewRef<T> {
get context(): T { return null !; }
}

function collectNativeNodes(lView: LView, parentTNode: TNode, result: any[]): any[] {
let tNodeChild = parentTNode.child;

while (tNodeChild) {
const nativeNode = getNativeByTNodeOrNull(tNodeChild, lView);
function collectNativeNodes(lView: LView, tNode: TNode | null, result: any[]): any[] {
while (tNode !== null) {
ngDevMode && assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection,
TNodeType.ElementContainer);
const nativeNode = getNativeByTNodeOrNull(tNode, lView);
nativeNode && result.push(nativeNode);
if (tNodeChild.type === TNodeType.ElementContainer) {
collectNativeNodes(lView, tNodeChild, result);
} else if (tNodeChild.type === TNodeType.Projection) {
if (tNode.type === TNodeType.ElementContainer) {
collectNativeNodes(lView, tNode.child, result);
} else if (tNode.type === TNodeType.Container) {
const lContainer = lView[tNode.index];
const containerTView = tNode.tViews !as TView;
ngDevMode && assertDefined(containerTView, 'TView expected');
const containerFirstChildTNode = containerTView.firstChild !;
if (containerFirstChildTNode !== null) {
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
collectNativeNodes(lContainer[i], containerFirstChildTNode, result);
}
}
} else if (tNode.type === TNodeType.Projection) {
const componentView = findComponentView(lView);
const componentHost = componentView[T_HOST] as TElementNode;
const parentView = getLViewParent(componentView);
let currentProjectedNode: TNode|null =
(componentHost.projection as(TNode | null)[])[tNodeChild.projection as number];
(componentHost.projection as(TNode | null)[])[tNode.projection as number];

while (currentProjectedNode && parentView) {
result.push(getNativeByTNode(currentProjectedNode, parentView));
currentProjectedNode = currentProjectedNode.next;
}
}
tNodeChild = tNodeChild.next;
tNode = tNode.next;
}

return result;
Expand Down
95 changes: 47 additions & 48 deletions packages/core/test/acceptance/template_ref_spec.ts
Expand Up @@ -13,47 +13,6 @@ import {onlyInIvy} from '@angular/private/testing';

describe('TemplateRef', () => {
describe('rootNodes', () => {
it('should include projected nodes in rootNodes', () => {
@Component({
selector: 'menu-content',
template: `
<ng-template>
Header
<ng-content></ng-content>
</ng-template>
`,
exportAs: 'menuContent'
})
class MenuContent {
@ViewChild(TemplateRef, {static: true}) template !: TemplateRef<any>;
}

@Component({
template: `
<menu-content #menu="menuContent">
<button>Item one</button>
<button>Item two</button>
</menu-content>
`
})
class App {
@ViewChild(MenuContent) content !: MenuContent;

constructor(public viewContainerRef: ViewContainerRef) {}
}

TestBed.configureTestingModule({declarations: [MenuContent, App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();

const instance = fixture.componentInstance;
const viewRef = instance.viewContainerRef.createEmbeddedView(instance.content.template);
const rootNodeTextContent = viewRef.rootNodes.map(node => node && node.textContent.trim())
.filter(text => text !== '');

expect(rootNodeTextContent).toEqual(['Header', 'Item one', 'Item two']);
});

it('should return root render nodes for an embedded view instance', () => {
@Component({
template: `
Expand Down Expand Up @@ -108,15 +67,56 @@ describe('TemplateRef', () => {
expect(embeddedView.rootNodes.length).toBe(0);
});

it('should not descend into containers when retrieving root nodes', () => {
it('should include projected nodes', () => {
@Component({
selector: 'menu-content',
template: `
<ng-template>
Header
<ng-content></ng-content>
</ng-template>
`,
exportAs: 'menuContent'
})
class MenuContent {
@ViewChild(TemplateRef, {static: true}) template !: TemplateRef<any>;
}

@Component({
template: `
<menu-content #menu="menuContent">
<button>Item one</button>
<button>Item two</button>
</menu-content>
`
})
class App {
@ViewChild(MenuContent) content !: MenuContent;

constructor(public viewContainerRef: ViewContainerRef) {}
}

TestBed.configureTestingModule({declarations: [MenuContent, App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();

const instance = fixture.componentInstance;
const viewRef = instance.viewContainerRef.createEmbeddedView(instance.content.template);
const rootNodeTextContent = viewRef.rootNodes.map(node => node && node.textContent.trim())
.filter(text => text !== '');

expect(rootNodeTextContent).toEqual(['Header', 'Item one', 'Item two']);
});

it('should descend into view containers on ng-template', () => {
/**
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
* additional `<!---->` comment, thus being slightly different than Ivy.
* (resulting in 1 root node in Ivy and 2 in VE).
*/
@Component({
template: `
<ng-template #templateRef><ng-template [ngIf]="true">text</ng-template>SUFFIX</ng-template>
<ng-template #templateRef><ng-template [ngIf]="true">text|</ng-template>SUFFIX</ng-template>
`
})
class App {
Expand All @@ -131,15 +131,14 @@ describe('TemplateRef', () => {
fixture.detectChanges();

const embeddedView = fixture.componentInstance.templateRef.createEmbeddedView({});
expect(embeddedView.rootNodes.length).toBe(2);
embeddedView.detectChanges();

expect(embeddedView.rootNodes.length).toBe(3);
expect(embeddedView.rootNodes[0].nodeType).toBe(Node.COMMENT_NODE);
expect(embeddedView.rootNodes[1].nodeType).toBe(Node.TEXT_NODE);
expect(embeddedView.rootNodes[2].nodeType).toBe(Node.TEXT_NODE);
});

/**
* Contrary to containers (<ng-template>) we _do_ descend into element containers
* (<ng-container>)
*/
it('should descend into element containers when retrieving root nodes', () => {
@Component({
template: `
Expand Down

0 comments on commit 502fb7e

Please sign in to comment.