Skip to content

Commit d994f85

Browse files
AndrewKushnirthePunderWoman
authored andcommitted
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
1 parent f37cb47 commit d994f85

File tree

3 files changed

+81
-22
lines changed

3 files changed

+81
-22
lines changed

packages/core/src/render3/collect_native_nodes.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
import {assertParentView} from './assert';
1010
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
11-
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
11+
import {CONTAINER_HEADER_OFFSET, NATIVE} from './interfaces/container';
1212
import {TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
1313
import {RNode} from './interfaces/renderer_dom';
1414
import {isLContainer} from './interfaces/type_checks';
15-
import {DECLARATION_COMPONENT_VIEW, LView, T_HOST, TVIEW, TView} from './interfaces/view';
15+
import {DECLARATION_COMPONENT_VIEW, HOST, LView, T_HOST, TVIEW, TView} from './interfaces/view';
1616
import {assertTNodeType} from './node_assert';
1717
import {getProjectionNodes} from './node_manipulation';
1818
import {getLViewParent} from './util/view_traversal_utils';
@@ -46,6 +46,22 @@ export function collectNativeNodes(
4646
lViewInAContainer[TVIEW], lViewInAContainer, lViewFirstChildTNode, result);
4747
}
4848
}
49+
50+
// When an LContainer is created, the anchor (comment) node is:
51+
// - (1) either reused in case of an ElementContainer (<ng-container>)
52+
// - (2) or a new comment node is created
53+
// In the first case, the anchor comment node would be added to the final
54+
// list by the code above (`result.push(unwrapRNode(lNode))`), but the second
55+
// case requires extra handling: the anchor node needs to be added to the
56+
// final list manually. See additional information in the `createAnchorNode`
57+
// function in the `view_container_ref.ts`.
58+
//
59+
// In the first case, the same reference would be stored in the `NATIVE`
60+
// and `HOST` slots in an LContainer. Otherwise, this is the second case and
61+
// we should add an element to the final list.
62+
if (lNode[NATIVE] !== lNode[HOST]) {
63+
result.push(lNode[NATIVE]);
64+
}
4965
}
5066

5167
const tNodeType = tNode.type;

packages/core/test/acceptance/template_ref_spec.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,6 @@ describe('TemplateRef', () => {
8888
});
8989

9090
it('should descend into view containers on ng-template', () => {
91-
/**
92-
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
93-
* additional `<!---->` comment, thus being slightly different than Ivy.
94-
* (resulting in 1 root node in Ivy and 2 in VE).
95-
*/
9691
const rootNodes = getRootNodes(`
9792
<ng-template #templateRef>
9893
<ng-template [ngIf]="true">text|</ng-template>SUFFIX
@@ -106,29 +101,31 @@ describe('TemplateRef', () => {
106101

107102
it('should descend into view containers on an element', () => {
108103
/**
109-
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
110-
* additional `<!---->` comment, thus being slightly different than Ivy.
111-
* (resulting in 1 root node in Ivy and 2 in VE).
104+
* Expected DOM structure:
105+
* ```
106+
* <div ng-reflect-ng-template-outlet="[object Object]"></div>
107+
* text
108+
* <!--container-->
109+
* SUFFIX
110+
* ```
112111
*/
113112
const rootNodes = getRootNodes(`
114-
<ng-template #dynamicTpl>text</ng-template>
115-
<ng-template #templateRef>
116-
<div [ngTemplateOutlet]="dynamicTpl"></div>SUFFIX
117-
</ng-template>
118-
`);
113+
<ng-template #dynamicTpl>text</ng-template>
114+
<ng-template #templateRef>
115+
<div [ngTemplateOutlet]="dynamicTpl"></div>SUFFIX
116+
</ng-template>
117+
`);
119118

120-
expect(rootNodes.length).toBe(3);
119+
expect(rootNodes.length).toBe(4);
121120
expect(rootNodes[0].nodeType).toBe(Node.ELEMENT_NODE);
122121
expect(rootNodes[1].nodeType).toBe(Node.TEXT_NODE);
123-
expect(rootNodes[2].nodeType).toBe(Node.TEXT_NODE);
122+
// This comment node is an anchor for the `ViewContainerRef`
123+
// created within the `NgTemplateOutlet` class.
124+
expect(rootNodes[2].nodeType).toBe(Node.COMMENT_NODE);
125+
expect(rootNodes[3].nodeType).toBe(Node.TEXT_NODE);
124126
});
125127

126128
it('should descend into view containers on ng-container', () => {
127-
/**
128-
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
129-
* additional `<!---->` comment, thus being slightly different than Ivy.
130-
* (resulting in 1 root node in Ivy and 2 in VE).
131-
*/
132129
const rootNodes = getRootNodes(`
133130
<ng-template #dynamicTpl>text</ng-template>
134131
<ng-template #templateRef><ng-container [ngTemplateOutlet]="dynamicTpl"></ng-container>SUFFIX</ng-template>

packages/platform-server/test/hydration_spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,52 @@ describe('platform-server integration', () => {
729729
verifyAllNodesClaimedForHydration(clientRootNode);
730730
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
731731
});
732+
733+
it('should support element containers with *ngIf', async () => {
734+
@Component({
735+
selector: 'cmp',
736+
standalone: true,
737+
template: 'Hi!',
738+
})
739+
class Cmp {
740+
}
741+
742+
@Component({
743+
standalone: true,
744+
selector: 'app',
745+
imports: [NgIf],
746+
template: `
747+
<ng-container *ngIf="true">
748+
<div #inner></div>
749+
</ng-container>
750+
<ng-template #outer />
751+
`,
752+
})
753+
class SimpleComponent {
754+
@ViewChild('inner', {read: ViewContainerRef}) inner!: ViewContainerRef;
755+
@ViewChild('outer', {read: ViewContainerRef}) outer!: ViewContainerRef;
756+
757+
ngAfterViewInit() {
758+
this.inner.createComponent(Cmp);
759+
this.outer.createComponent(Cmp);
760+
}
761+
}
762+
763+
const html = await ssr(SimpleComponent);
764+
const ssrContents = getAppContents(html);
765+
766+
expect(ssrContents).toContain(`<app ${NGH_ATTR_NAME}`);
767+
768+
resetTViewsFor(SimpleComponent, Cmp);
769+
770+
const appRef = await hydrate(html, SimpleComponent);
771+
const compRef = getComponentRef<SimpleComponent>(appRef);
772+
appRef.tick();
773+
774+
const clientRootNode = compRef.location.nativeElement;
775+
verifyAllNodesClaimedForHydration(clientRootNode);
776+
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
777+
});
732778
});
733779

734780
describe('view containers', () => {

0 commit comments

Comments
 (0)