Skip to content

Commit

Permalink
fix(core): handle hydration of view containers that use component hos…
Browse files Browse the repository at this point in the history
…ts 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
  • Loading branch information
AndrewKushnir authored and thePunderWoman committed Aug 29, 2023
1 parent 14941b9 commit 20d6260
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 52 deletions.
87 changes: 62 additions & 25 deletions packages/core/src/hydration/annotate.ts
Expand Up @@ -9,14 +9,14 @@
import {ApplicationRef} from '../application_ref';
import {ViewEncapsulation} from '../metadata';
import {Renderer2} from '../render';
import {collectNativeNodes} from '../render3/collect_native_nodes';
import {collectNativeNodes, collectNativeNodesInLContainer} from '../render3/collect_native_nodes';
import {getComponentDef} from '../render3/definition';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
import {TNode, TNodeType} from '../render3/interfaces/node';
import {RElement} from '../render3/interfaces/renderer_dom';
import {hasI18n, isComponentHost, isLContainer, isProjectionTNode, isRootView} from '../render3/interfaces/type_checks';
import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
import {unwrapRNode} from '../render3/util/view_utils';
import {unwrapLView, unwrapRNode} from '../render3/util/view_utils';
import {TransferState} from '../transfer_state';

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

/**
* Computes the number of root nodes in all views in a given LContainer.
*/
function calcNumRootNodesInLContainer(lContainer: LContainer): number {
const rootNodes: unknown[] = [];
collectNativeNodesInLContainer(lContainer, rootNodes);
return rootNodes.length;
}


/**
* Annotates root level component's LView for hydration,
* see `annotateHostElementForHydration` for additional information.
Expand All @@ -113,7 +123,7 @@ function annotateComponentLViewForHydration(lView: LView, context: HydrationCont
* container.
*/
function annotateLContainerForHydration(lContainer: LContainer, context: HydrationContext) {
const componentLView = lContainer[HOST] as LView<unknown>;
const componentLView = unwrapLView(lContainer[HOST]) as LView<unknown>;

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

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

// If this is a root view, get an LView for the underlying component,
// because it contains information about the view to serialize.
let template: string;
let numRootNodes: number;
let serializedView: SerializedContainerView|undefined;

if (isRootView(childLView)) {
// If this is a root view, get an LView for the underlying component,
// because it contains information about the view to serialize.
childLView = childLView[HEADER_OFFSET];

// If we have an LContainer at this position, this indicates that the
// host element was used as a ViewContainerRef anchor (e.g. a `ViewContainerRef`
// was injected within the component class). This case requires special handling.
if (isLContainer(childLView)) {
// Calculate the number of root nodes in all views in a given container
// and increment by one to account for an anchor node itself, i.e. in this
// scenario we'll have a layout that would look like this:
// `<app-root /><#VIEW1><#VIEW2>...<!--container-->`
// The `+1` is to capture the `<app-root />` element.
numRootNodes = calcNumRootNodesInLContainer(childLView) + 1;

annotateLContainerForHydration(childLView, context);

const componentLView = unwrapLView(childLView[HOST]) as LView<unknown>;

serializedView = {
[TEMPLATE_ID]: componentLView[TVIEW].ssrId!,
[NUM_ROOT_NODES]: numRootNodes,
};
}
}
const childTView = childLView[TVIEW];

let template: string;
let numRootNodes = 0;
if (childTView.type === TViewType.Component) {
template = childTView.ssrId!;
if (!serializedView) {
const childTView = childLView[TVIEW];

// This is a component view, thus it has only 1 root node: the component
// host node itself (other nodes would be inside that host node).
numRootNodes = 1;
} else {
template = getSsrId(childTView);
numRootNodes = calcNumRootNodes(childTView, childLView, childTView.firstChild);
}
if (childTView.type === TViewType.Component) {
template = childTView.ssrId!;

// This is a component view, thus it has only 1 root node: the component
// host node itself (other nodes would be inside that host node).
numRootNodes = 1;
} else {
template = getSsrId(childTView);
numRootNodes = calcNumRootNodes(childTView, childLView, childTView.firstChild);
}

const view: SerializedContainerView = {
[TEMPLATE_ID]: template,
[NUM_ROOT_NODES]: numRootNodes,
...serializeLView(lContainer[i] as LView, context),
};
serializedView = {
[TEMPLATE_ID]: template,
[NUM_ROOT_NODES]: numRootNodes,
...serializeLView(lContainer[i] as LView, context),
};
}

// Check if the previous view has the same shape (for example, it was
// produced by the *ngFor), in which case bump the counter on the previous
// view instead of including the same information again.
const currentViewAsString = JSON.stringify(view);
const currentViewAsString = JSON.stringify(serializedView);
if (views.length > 0 && currentViewAsString === lastViewAsString) {
const previousView = views[views.length - 1];
previousView[MULTIPLIER] ??= 1;
previousView[MULTIPLIER]++;
} else {
// Record this view as most recently added.
lastViewAsString = currentViewAsString;
views.push(view);
views.push(serializedView);
}
}
return views;
Expand Down Expand Up @@ -355,6 +391,7 @@ function serializeLView(lView: LView, context: HydrationContext): SerializedView
annotateHostElementForHydration(targetNode, hostNode as LView, context);
}
}

ngh[CONTAINERS] ??= {};
ngh[CONTAINERS][noOffsetIndex] = serializeLContainer(lView[i], context);
} else if (Array.isArray(lView[i])) {
Expand Down
60 changes: 33 additions & 27 deletions packages/core/src/render3/collect_native_nodes.ts
Expand Up @@ -8,18 +8,17 @@

import {assertParentView} from './assert';
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
import {CONTAINER_HEADER_OFFSET, NATIVE} from './interfaces/container';
import {CONTAINER_HEADER_OFFSET, LContainer, 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, HOST, LView, T_HOST, TVIEW, TView} from './interfaces/view';
import {DECLARATION_COMPONENT_VIEW, HOST, LView, TVIEW, TView} from './interfaces/view';
import {assertTNodeType} from './node_assert';
import {getProjectionNodes} from './node_manipulation';
import {getLViewParent} from './util/view_traversal_utils';
import {unwrapRNode} from './util/view_utils';



export function collectNativeNodes(
tView: TView, lView: LView, tNode: TNode|null, result: any[],
isProjection: boolean = false): any[] {
Expand All @@ -38,30 +37,7 @@ export function collectNativeNodes(
// ViewContainerRef). When we find a LContainer we need to descend into it to collect root nodes
// from the views in this container.
if (isLContainer(lNode)) {
for (let i = CONTAINER_HEADER_OFFSET; i < lNode.length; i++) {
const lViewInAContainer = lNode[i];
const lViewFirstChildTNode = lViewInAContainer[TVIEW].firstChild;
if (lViewFirstChildTNode !== null) {
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]);
}
collectNativeNodesInLContainer(lNode, result);
}

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

return result;
}

/**
* Collects all root nodes in all views in a given LContainer.
*/
export function collectNativeNodesInLContainer(lContainer: LContainer, result: any[]) {
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
const lViewInAContainer = lContainer[i];
const lViewFirstChildTNode = lViewInAContainer[TVIEW].firstChild;
if (lViewFirstChildTNode !== null) {
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 in the `collectNativeNodes` function
// (see the `result.push(unwrapRNode(lNode))` line), 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 (lContainer[NATIVE] !== lContainer[HOST]) {
result.push(lContainer[NATIVE]);
}
}
Expand Up @@ -716,6 +716,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
Expand Down
Expand Up @@ -773,6 +773,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
Expand Down
Expand Up @@ -587,6 +587,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
Expand Down
Expand Up @@ -785,6 +785,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "collectStylingFromDirectives"
},
Expand Down
Expand Up @@ -764,6 +764,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "collectStylingFromDirectives"
},
Expand Down
Expand Up @@ -452,6 +452,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
Expand Down
Expand Up @@ -647,6 +647,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Expand Up @@ -941,6 +941,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "collectQueryResults"
},
Expand Down
Expand Up @@ -530,6 +530,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "commitLViewConsumerIfHasProducers"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/todo/bundle.golden_symbols.json
Expand Up @@ -686,6 +686,9 @@
{
"name": "collectNativeNodes"
},
{
"name": "collectNativeNodesInLContainer"
},
{
"name": "collectStylingFromDirectives"
},
Expand Down

0 comments on commit 20d6260

Please sign in to comment.