Skip to content

Commit 20d6260

Browse files
AndrewKushnirthePunderWoman
authored andcommitted
fix(core): handle hydration of view containers that use component hosts as anchors (#51456)
This commit fixes an issue where serialization of a view container fails in case it uses a component host as an anchor. This fix is similar to the fix from #51247, but for cases when we insert a component (that acts as a host for a view container) deeper in a hierarchy. Resolves #51318. PR Close #51456
1 parent 14941b9 commit 20d6260

File tree

13 files changed

+245
-52
lines changed

13 files changed

+245
-52
lines changed

packages/core/src/hydration/annotate.ts

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
import {ApplicationRef} from '../application_ref';
1010
import {ViewEncapsulation} from '../metadata';
1111
import {Renderer2} from '../render';
12-
import {collectNativeNodes} from '../render3/collect_native_nodes';
12+
import {collectNativeNodes, collectNativeNodesInLContainer} from '../render3/collect_native_nodes';
1313
import {getComponentDef} from '../render3/definition';
1414
import {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
1515
import {TNode, TNodeType} from '../render3/interfaces/node';
1616
import {RElement} from '../render3/interfaces/renderer_dom';
1717
import {hasI18n, isComponentHost, isLContainer, isProjectionTNode, isRootView} from '../render3/interfaces/type_checks';
1818
import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
19-
import {unwrapRNode} from '../render3/util/view_utils';
19+
import {unwrapLView, unwrapRNode} from '../render3/util/view_utils';
2020
import {TransferState} from '../transfer_state';
2121

2222
import {unsupportedProjectionOfDomNodes} from './error_handling';
@@ -92,6 +92,16 @@ function calcNumRootNodes(tView: TView, lView: LView, tNode: TNode|null): number
9292
return rootNodes.length;
9393
}
9494

95+
/**
96+
* Computes the number of root nodes in all views in a given LContainer.
97+
*/
98+
function calcNumRootNodesInLContainer(lContainer: LContainer): number {
99+
const rootNodes: unknown[] = [];
100+
collectNativeNodesInLContainer(lContainer, rootNodes);
101+
return rootNodes.length;
102+
}
103+
104+
95105
/**
96106
* Annotates root level component's LView for hydration,
97107
* see `annotateHostElementForHydration` for additional information.
@@ -113,7 +123,7 @@ function annotateComponentLViewForHydration(lView: LView, context: HydrationCont
113123
* container.
114124
*/
115125
function annotateLContainerForHydration(lContainer: LContainer, context: HydrationContext) {
116-
const componentLView = lContainer[HOST] as LView<unknown>;
126+
const componentLView = unwrapLView(lContainer[HOST]) as LView<unknown>;
117127

118128
// Serialize the root component itself.
119129
const componentLViewNghIndex = annotateComponentLViewForHydration(componentLView, context);
@@ -191,49 +201,75 @@ export function annotateForHydration(appRef: ApplicationRef, doc: Document) {
191201
function serializeLContainer(
192202
lContainer: LContainer, context: HydrationContext): SerializedContainerView[] {
193203
const views: SerializedContainerView[] = [];
194-
let lastViewAsString: string = '';
204+
let lastViewAsString = '';
195205

196206
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
197207
let childLView = lContainer[i] as LView;
198208

199-
// If this is a root view, get an LView for the underlying component,
200-
// because it contains information about the view to serialize.
209+
let template: string;
210+
let numRootNodes: number;
211+
let serializedView: SerializedContainerView|undefined;
212+
201213
if (isRootView(childLView)) {
214+
// If this is a root view, get an LView for the underlying component,
215+
// because it contains information about the view to serialize.
202216
childLView = childLView[HEADER_OFFSET];
217+
218+
// If we have an LContainer at this position, this indicates that the
219+
// host element was used as a ViewContainerRef anchor (e.g. a `ViewContainerRef`
220+
// was injected within the component class). This case requires special handling.
221+
if (isLContainer(childLView)) {
222+
// Calculate the number of root nodes in all views in a given container
223+
// and increment by one to account for an anchor node itself, i.e. in this
224+
// scenario we'll have a layout that would look like this:
225+
// `<app-root /><#VIEW1><#VIEW2>...<!--container-->`
226+
// The `+1` is to capture the `<app-root />` element.
227+
numRootNodes = calcNumRootNodesInLContainer(childLView) + 1;
228+
229+
annotateLContainerForHydration(childLView, context);
230+
231+
const componentLView = unwrapLView(childLView[HOST]) as LView<unknown>;
232+
233+
serializedView = {
234+
[TEMPLATE_ID]: componentLView[TVIEW].ssrId!,
235+
[NUM_ROOT_NODES]: numRootNodes,
236+
};
237+
}
203238
}
204-
const childTView = childLView[TVIEW];
205239

206-
let template: string;
207-
let numRootNodes = 0;
208-
if (childTView.type === TViewType.Component) {
209-
template = childTView.ssrId!;
240+
if (!serializedView) {
241+
const childTView = childLView[TVIEW];
210242

211-
// This is a component view, thus it has only 1 root node: the component
212-
// host node itself (other nodes would be inside that host node).
213-
numRootNodes = 1;
214-
} else {
215-
template = getSsrId(childTView);
216-
numRootNodes = calcNumRootNodes(childTView, childLView, childTView.firstChild);
217-
}
243+
if (childTView.type === TViewType.Component) {
244+
template = childTView.ssrId!;
245+
246+
// This is a component view, thus it has only 1 root node: the component
247+
// host node itself (other nodes would be inside that host node).
248+
numRootNodes = 1;
249+
} else {
250+
template = getSsrId(childTView);
251+
numRootNodes = calcNumRootNodes(childTView, childLView, childTView.firstChild);
252+
}
218253

219-
const view: SerializedContainerView = {
220-
[TEMPLATE_ID]: template,
221-
[NUM_ROOT_NODES]: numRootNodes,
222-
...serializeLView(lContainer[i] as LView, context),
223-
};
254+
serializedView = {
255+
[TEMPLATE_ID]: template,
256+
[NUM_ROOT_NODES]: numRootNodes,
257+
...serializeLView(lContainer[i] as LView, context),
258+
};
259+
}
224260

225261
// Check if the previous view has the same shape (for example, it was
226262
// produced by the *ngFor), in which case bump the counter on the previous
227263
// view instead of including the same information again.
228-
const currentViewAsString = JSON.stringify(view);
264+
const currentViewAsString = JSON.stringify(serializedView);
229265
if (views.length > 0 && currentViewAsString === lastViewAsString) {
230266
const previousView = views[views.length - 1];
231267
previousView[MULTIPLIER] ??= 1;
232268
previousView[MULTIPLIER]++;
233269
} else {
234270
// Record this view as most recently added.
235271
lastViewAsString = currentViewAsString;
236-
views.push(view);
272+
views.push(serializedView);
237273
}
238274
}
239275
return views;
@@ -355,6 +391,7 @@ function serializeLView(lView: LView, context: HydrationContext): SerializedView
355391
annotateHostElementForHydration(targetNode, hostNode as LView, context);
356392
}
357393
}
394+
358395
ngh[CONTAINERS] ??= {};
359396
ngh[CONTAINERS][noOffsetIndex] = serializeLContainer(lView[i], context);
360397
} else if (Array.isArray(lView[i])) {

packages/core/src/render3/collect_native_nodes.ts

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@
88

99
import {assertParentView} from './assert';
1010
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
11-
import {CONTAINER_HEADER_OFFSET, NATIVE} from './interfaces/container';
11+
import {CONTAINER_HEADER_OFFSET, LContainer, 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, HOST, LView, T_HOST, TVIEW, TView} from './interfaces/view';
15+
import {DECLARATION_COMPONENT_VIEW, HOST, LView, 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';
1919
import {unwrapRNode} from './util/view_utils';
2020

2121

22-
2322
export function collectNativeNodes(
2423
tView: TView, lView: LView, tNode: TNode|null, result: any[],
2524
isProjection: boolean = false): any[] {
@@ -38,30 +37,7 @@ export function collectNativeNodes(
3837
// ViewContainerRef). When we find a LContainer we need to descend into it to collect root nodes
3938
// from the views in this container.
4039
if (isLContainer(lNode)) {
41-
for (let i = CONTAINER_HEADER_OFFSET; i < lNode.length; i++) {
42-
const lViewInAContainer = lNode[i];
43-
const lViewFirstChildTNode = lViewInAContainer[TVIEW].firstChild;
44-
if (lViewFirstChildTNode !== null) {
45-
collectNativeNodes(
46-
lViewInAContainer[TVIEW], lViewInAContainer, lViewFirstChildTNode, result);
47-
}
48-
}
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-
}
40+
collectNativeNodesInLContainer(lNode, result);
6541
}
6642

6743
const tNodeType = tNode.type;
@@ -88,3 +64,33 @@ export function collectNativeNodes(
8864

8965
return result;
9066
}
67+
68+
/**
69+
* Collects all root nodes in all views in a given LContainer.
70+
*/
71+
export function collectNativeNodesInLContainer(lContainer: LContainer, result: any[]) {
72+
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
73+
const lViewInAContainer = lContainer[i];
74+
const lViewFirstChildTNode = lViewInAContainer[TVIEW].firstChild;
75+
if (lViewFirstChildTNode !== null) {
76+
collectNativeNodes(lViewInAContainer[TVIEW], lViewInAContainer, lViewFirstChildTNode, result);
77+
}
78+
}
79+
80+
// When an LContainer is created, the anchor (comment) node is:
81+
// - (1) either reused in case of an ElementContainer (<ng-container>)
82+
// - (2) or a new comment node is created
83+
// In the first case, the anchor comment node would be added to the final
84+
// list by the code in the `collectNativeNodes` function
85+
// (see the `result.push(unwrapRNode(lNode))` line), but the second
86+
// case requires extra handling: the anchor node needs to be added to the
87+
// final list manually. See additional information in the `createAnchorNode`
88+
// function in the `view_container_ref.ts`.
89+
//
90+
// In the first case, the same reference would be stored in the `NATIVE`
91+
// and `HOST` slots in an LContainer. Otherwise, this is the second case and
92+
// we should add an element to the final list.
93+
if (lContainer[NATIVE] !== lContainer[HOST]) {
94+
result.push(lContainer[NATIVE]);
95+
}
96+
}

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,9 @@
716716
{
717717
"name": "collectNativeNodes"
718718
},
719+
{
720+
"name": "collectNativeNodesInLContainer"
721+
},
719722
{
720723
"name": "commitLViewConsumerIfHasProducers"
721724
},

packages/core/test/bundling/animations/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,9 @@
773773
{
774774
"name": "collectNativeNodes"
775775
},
776+
{
777+
"name": "collectNativeNodesInLContainer"
778+
},
776779
{
777780
"name": "commitLViewConsumerIfHasProducers"
778781
},

packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,9 @@
587587
{
588588
"name": "collectNativeNodes"
589589
},
590+
{
591+
"name": "collectNativeNodesInLContainer"
592+
},
590593
{
591594
"name": "commitLViewConsumerIfHasProducers"
592595
},

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,9 @@
785785
{
786786
"name": "collectNativeNodes"
787787
},
788+
{
789+
"name": "collectNativeNodesInLContainer"
790+
},
788791
{
789792
"name": "collectStylingFromDirectives"
790793
},

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,9 @@
764764
{
765765
"name": "collectNativeNodes"
766766
},
767+
{
768+
"name": "collectNativeNodesInLContainer"
769+
},
767770
{
768771
"name": "collectStylingFromDirectives"
769772
},

packages/core/test/bundling/hello_world/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,9 @@
452452
{
453453
"name": "collectNativeNodes"
454454
},
455+
{
456+
"name": "collectNativeNodesInLContainer"
457+
},
455458
{
456459
"name": "commitLViewConsumerIfHasProducers"
457460
},

packages/core/test/bundling/hydration/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,9 @@
647647
{
648648
"name": "collectNativeNodes"
649649
},
650+
{
651+
"name": "collectNativeNodesInLContainer"
652+
},
650653
{
651654
"name": "commitLViewConsumerIfHasProducers"
652655
},

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,9 @@
941941
{
942942
"name": "collectNativeNodes"
943943
},
944+
{
945+
"name": "collectNativeNodesInLContainer"
946+
},
944947
{
945948
"name": "collectQueryResults"
946949
},

0 commit comments

Comments
 (0)