fix(ivy): descend into view containers on ng-template when collecting rootNodes (#33493)
PR Close #33493
This commit is contained in:
parent
f197191a5f
commit
502fb7e307
@ -10,10 +10,13 @@ import {ApplicationRef} from '../application_ref';
|
|||||||
import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref';
|
import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref';
|
||||||
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_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 {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 {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 {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 {destroyLView, renderDetachView} from './node_manipulation';
|
||||||
import {findComponentView, getLViewParent} from './util/view_traversal_utils';
|
import {findComponentView, getLViewParent} from './util/view_traversal_utils';
|
||||||
import {getNativeByTNode, getNativeByTNodeOrNull} from './util/view_utils';
|
import {getNativeByTNode, getNativeByTNodeOrNull} from './util/view_utils';
|
||||||
@ -38,7 +41,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
|
|||||||
get rootNodes(): any[] {
|
get rootNodes(): any[] {
|
||||||
if (this._lView[HOST] == null) {
|
if (this._lView[HOST] == null) {
|
||||||
const tView = this._lView[T_HOST] as TViewNode;
|
const tView = this._lView[T_HOST] as TViewNode;
|
||||||
return collectNativeNodes(this._lView, tView, []);
|
return collectNativeNodes(this._lView, tView.child, []);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -299,27 +302,38 @@ export class RootViewRef<T> extends ViewRef<T> {
|
|||||||
get context(): T { return null !; }
|
get context(): T { return null !; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectNativeNodes(lView: LView, parentTNode: TNode, result: any[]): any[] {
|
function collectNativeNodes(lView: LView, tNode: TNode | null, result: any[]): any[] {
|
||||||
let tNodeChild = parentTNode.child;
|
while (tNode !== null) {
|
||||||
|
ngDevMode && assertNodeOfPossibleTypes(
|
||||||
while (tNodeChild) {
|
tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection,
|
||||||
const nativeNode = getNativeByTNodeOrNull(tNodeChild, lView);
|
TNodeType.ElementContainer);
|
||||||
|
const nativeNode = getNativeByTNodeOrNull(tNode, lView);
|
||||||
nativeNode && result.push(nativeNode);
|
nativeNode && result.push(nativeNode);
|
||||||
if (tNodeChild.type === TNodeType.ElementContainer) {
|
if (tNode.type === TNodeType.ElementContainer) {
|
||||||
collectNativeNodes(lView, tNodeChild, result);
|
collectNativeNodes(lView, tNode.child, result);
|
||||||
} else if (tNodeChild.type === TNodeType.Projection) {
|
} 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 componentView = findComponentView(lView);
|
||||||
const componentHost = componentView[T_HOST] as TElementNode;
|
const componentHost = componentView[T_HOST] as TElementNode;
|
||||||
const parentView = getLViewParent(componentView);
|
const parentView = getLViewParent(componentView);
|
||||||
let currentProjectedNode: TNode|null =
|
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) {
|
while (currentProjectedNode && parentView) {
|
||||||
result.push(getNativeByTNode(currentProjectedNode, parentView));
|
result.push(getNativeByTNode(currentProjectedNode, parentView));
|
||||||
currentProjectedNode = currentProjectedNode.next;
|
currentProjectedNode = currentProjectedNode.next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tNodeChild = tNodeChild.next;
|
tNode = tNode.next;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -13,47 +13,6 @@ import {onlyInIvy} from '@angular/private/testing';
|
|||||||
|
|
||||||
describe('TemplateRef', () => {
|
describe('TemplateRef', () => {
|
||||||
describe('rootNodes', () => {
|
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', () => {
|
it('should return root render nodes for an embedded view instance', () => {
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@ -108,7 +67,48 @@ describe('TemplateRef', () => {
|
|||||||
expect(embeddedView.rootNodes.length).toBe(0);
|
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
|
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
|
||||||
* additional `<!---->` comment, thus being slightly different than Ivy.
|
* additional `<!---->` comment, thus being slightly different than Ivy.
|
||||||
@ -116,7 +116,7 @@ describe('TemplateRef', () => {
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
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 {
|
class App {
|
||||||
@ -131,15 +131,14 @@ describe('TemplateRef', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const embeddedView = fixture.componentInstance.templateRef.createEmbeddedView({});
|
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[0].nodeType).toBe(Node.COMMENT_NODE);
|
||||||
expect(embeddedView.rootNodes[1].nodeType).toBe(Node.TEXT_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', () => {
|
it('should descend into element containers when retrieving root nodes', () => {
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
|
Loading…
x
Reference in New Issue
Block a user