From fedc75624c5dcfaaa2b5ef901e7e700309770a26 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Sat, 15 Apr 2023 14:24:13 -0700 Subject: [PATCH] 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 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 --- .../core/src/render3/collect_native_nodes.ts | 20 +++++++- .../core/test/acceptance/template_ref_spec.ts | 37 +++++++-------- .../platform-server/test/hydration_spec.ts | 46 +++++++++++++++++++ 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/packages/core/src/render3/collect_native_nodes.ts b/packages/core/src/render3/collect_native_nodes.ts index 8b5881232a1..5905162faa9 100644 --- a/packages/core/src/render3/collect_native_nodes.ts +++ b/packages/core/src/render3/collect_native_nodes.ts @@ -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 () + // - (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; diff --git a/packages/core/test/acceptance/template_ref_spec.ts b/packages/core/test/acceptance/template_ref_spec.ts index c9b8d918144..402ef1f05aa 100644 --- a/packages/core/test/acceptance/template_ref_spec.ts +++ b/packages/core/test/acceptance/template_ref_spec.ts @@ -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(` text|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: + * ``` + *
+ * text + * + * SUFFIX + * ``` */ const rootNodes = getRootNodes(` - text - -
SUFFIX -
- `); + text + +
SUFFIX +
+ `); - 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(` text SUFFIX diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index afbcc77abb4..a659e8d440f 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -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: ` + +
+
+ + `, + }) + 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(`(appRef); + appRef.tick(); + + const clientRootNode = compRef.location.nativeElement; + verifyAllNodesClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); }); describe('view containers', () => {