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

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 angular#51247, but for cases when we insert a component (that acts as a host for a view container) deeper in a hierarchy.

Resolves angular#51318.
  • Loading branch information
AndrewKushnir committed Aug 22, 2023
1 parent d2346a6 commit f9d02a7
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 @@ -932,6 +932,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 f9d02a7

Please sign in to comment.