fix(core): include inner ViewContainerRef anchor nodes into ViewRef.rootNodes output (#49867)

Currently, the `ViewRef.rootNodes` output is missing anchor (comment) nodes for inner `ViewContainerRef`s,
when an achor node was created for that instance of a `ViewContainerRef` (which happens in all cases except
when an <ng-container> was used as a host for a view container).

This issue affects hydration logic, which relies on the number of root nodes within a view to properly determine
segments in DOM that belong to a particular view.

Resolves #49849.

PR Close #49867
This commit is contained in:
Andrew Kushnir
2023-04-15 14:24:13 -07:00
committed by Jessica Janiuk
parent e341dbdb78
commit fedc75624c
3 changed files with 81 additions and 22 deletions
@@ -8,11 +8,11 @@
import {assertParentView} from './assert';
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
import {CONTAINER_HEADER_OFFSET, NATIVE} from './interfaces/container';
import {TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
import {RNode} from './interfaces/renderer_dom';
import {isLContainer} from './interfaces/type_checks';
import {DECLARATION_COMPONENT_VIEW, LView, T_HOST, TVIEW, TView} from './interfaces/view';
import {DECLARATION_COMPONENT_VIEW, HOST, LView, T_HOST, TVIEW, TView} from './interfaces/view';
import {assertTNodeType} from './node_assert';
import {getProjectionNodes} from './node_manipulation';
import {getLViewParent} from './util/view_traversal_utils';
@@ -46,6 +46,22 @@ export function collectNativeNodes(
lViewInAContainer[TVIEW], lViewInAContainer, lViewFirstChildTNode, result);
}
}
// When an LContainer is created, the anchor (comment) node is:
// - (1) either reused in case of an ElementContainer (<ng-container>)
// - (2) or a new comment node is created
// In the first case, the anchor comment node would be added to the final
// list by the code above (`result.push(unwrapRNode(lNode))`), but the second
// case requires extra handling: the anchor node needs to be added to the
// final list manually. See additional information in the `createAnchorNode`
// function in the `view_container_ref.ts`.
//
// In the first case, the same reference would be stored in the `NATIVE`
// and `HOST` slots in an LContainer. Otherwise, this is the second case and
// we should add an element to the final list.
if (lNode[NATIVE] !== lNode[HOST]) {
result.push(lNode[NATIVE]);
}
}
const tNodeType = tNode.type;
@@ -88,11 +88,6 @@ describe('TemplateRef', () => {
});
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).
*/
const rootNodes = getRootNodes(`
<ng-template #templateRef>
<ng-template [ngIf]="true">text|</ng-template>SUFFIX
@@ -106,29 +101,31 @@ describe('TemplateRef', () => {
it('should descend into view containers on an element', () => {
/**
* 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).
* Expected DOM structure:
* ```
* <div ng-reflect-ng-template-outlet="[object Object]"></div>
* text
* <!--container-->
* SUFFIX
* ```
*/
const rootNodes = getRootNodes(`
<ng-template #dynamicTpl>text</ng-template>
<ng-template #templateRef>
<div [ngTemplateOutlet]="dynamicTpl"></div>SUFFIX
</ng-template>
`);
<ng-template #dynamicTpl>text</ng-template>
<ng-template #templateRef>
<div [ngTemplateOutlet]="dynamicTpl"></div>SUFFIX
</ng-template>
`);
expect(rootNodes.length).toBe(3);
expect(rootNodes.length).toBe(4);
expect(rootNodes[0].nodeType).toBe(Node.ELEMENT_NODE);
expect(rootNodes[1].nodeType).toBe(Node.TEXT_NODE);
expect(rootNodes[2].nodeType).toBe(Node.TEXT_NODE);
// This comment node is an anchor for the `ViewContainerRef`
// created within the `NgTemplateOutlet` class.
expect(rootNodes[2].nodeType).toBe(Node.COMMENT_NODE);
expect(rootNodes[3].nodeType).toBe(Node.TEXT_NODE);
});
it('should descend into view containers on ng-container', () => {
/**
* 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).
*/
const rootNodes = getRootNodes(`
<ng-template #dynamicTpl>text</ng-template>
<ng-template #templateRef><ng-container [ngTemplateOutlet]="dynamicTpl"></ng-container>SUFFIX</ng-template>
@@ -729,6 +729,52 @@ describe('platform-server integration', () => {
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
});
it('should support element containers with *ngIf', async () => {
@Component({
selector: 'cmp',
standalone: true,
template: 'Hi!',
})
class Cmp {
}
@Component({
standalone: true,
selector: 'app',
imports: [NgIf],
template: `
<ng-container *ngIf="true">
<div #inner></div>
</ng-container>
<ng-template #outer />
`,
})
class SimpleComponent {
@ViewChild('inner', {read: ViewContainerRef}) inner!: ViewContainerRef;
@ViewChild('outer', {read: ViewContainerRef}) outer!: ViewContainerRef;
ngAfterViewInit() {
this.inner.createComponent(Cmp);
this.outer.createComponent(Cmp);
}
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain(`<app ${NGH_ATTR_NAME}`);
resetTViewsFor(SimpleComponent, Cmp);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
const clientRootNode = compRef.location.nativeElement;
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
});
});
describe('view containers', () => {