Skip to content

Commit

Permalink
fix(core): handle hydration of view containers for root components
Browse files Browse the repository at this point in the history
For cases when a root component also acts as an anchor node for a ViewContainerRef (for example, when ViewContainerRef is injected in a root component), there is a need to serialize information about the component itself, as well as an LContainer that represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info: (1) hydration info for the root component itself and (2) hydration info for the ViewContainerRef instance (an LContainer). Each piece of information is included into the hydration data (in the TransferState object) separately, thus we end up with 2 ids. Since we only have 1 root element, we encode both bits of info into a single string: ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view and 25 is the `ngh` for a root view which holds LContainer).

Previously, we were only including component-related information, thus all the views in the view container remained dehydrated and duplicated (client-rendered from scratch) on the client.

Resolves angular#51157.
  • Loading branch information
AndrewKushnir committed Aug 4, 2023
1 parent 0a38dc3 commit 47e33d2
Show file tree
Hide file tree
Showing 6 changed files with 451 additions and 52 deletions.
85 changes: 69 additions & 16 deletions packages/core/src/hydration/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@

import {ApplicationRef} from '../application_ref';
import {ViewEncapsulation} from '../metadata';
import {Renderer2} from '../render';
import {collectNativeNodes} 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, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
import {unwrapRNode} from '../render3/util/view_utils';
import {TransferState} from '../transfer_state';

import {unsupportedProjectionOfDomNodes} from './error_handling';
import {CONTAINERS, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NODES, NUM_ROOT_NODES, SerializedContainerView, SerializedView, TEMPLATE_ID, TEMPLATES} from './interfaces';
import {calcPathForNode} from './node_lookup_utils';
import {hasInSkipHydrationBlockFlag, isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
import {getComponentLViewForHydration, NGH_ATTR_NAME, NGH_DATA_KEY, TextNodeMarker} from './utils';
import {isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
import {getLNodeForHydration, NGH_ATTR_NAME, NGH_DATA_KEY, TextNodeMarker} from './utils';

/**
* A collection that tracks all serialized views (`ngh` DOM annotations)
Expand Down Expand Up @@ -91,6 +92,54 @@ function calcNumRootNodes(tView: TView, lView: LView, tNode: TNode|null): number
return rootNodes.length;
}

/**
* Annotates root level component's LView for hydration,
* see `annotateHostElementForHydration` for additional information.
*/
function annotateComponentLViewForHydration(lView: LView, context: HydrationContext): number|null {
const hostElement = lView[HOST];
// Root elements might also be annotated with the `ngSkipHydration` attribute,
// check if it's present before starting the serialization process.
if (hostElement && !(hostElement as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
return annotateHostElementForHydration(hostElement as HTMLElement, lView, context);
}
return null;
}

/**
* Annotates root level LContainer for hydration. This happens when a root component
* injects ViewContainerRef, thus making the component an anchor for a view container.
* This function serializes the component itself as well as all views from the view
* container.
*/
function annotateLContainerForHydration(lContainer: LContainer, context: HydrationContext) {
const componentLView = lContainer[HOST] as LView<unknown>;

// Serialize the root component itself.
const componentLViewNghIndex = annotateComponentLViewForHydration(componentLView, context);

const hostElement = unwrapRNode(componentLView[HOST]!) as HTMLElement;

// Serialize all views within this view container.
const rootLView = lContainer[PARENT];
const rootLViewNghIndex = annotateHostElementForHydration(hostElement, rootLView, context);

const renderer = componentLView[RENDERER] as Renderer2;

// For cases when a root component also acts as an anchor node for a ViewContainerRef
// (for example, when ViewContainerRef is injected in a root component), there is a need
// to serialize information about the component itself, as well as an LContainer that
// represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info:
// (1) hydration info for the root component itself and (2) hydration info for the
// ViewContainerRef instance (an LContainer). Each piece of information is included into
// the hydration data (in the TransferState object) separately, thus we end up with 2 ids.
// Since we only have 1 root element, we encode both bits of info into a single string:
// ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view
// and 25 is the `ngh` for a root view which holds LContainer).
const finalIndex = `${componentLViewNghIndex}|${rootLViewNghIndex}`;
renderer.setAttribute(hostElement, NGH_ATTR_NAME, finalIndex);
}

/**
* Annotates all components bootstrapped in a given ApplicationRef
* with info needed for hydration.
Expand All @@ -103,21 +152,21 @@ export function annotateForHydration(appRef: ApplicationRef, doc: Document) {
const corruptedTextNodes = new Map<HTMLElement, TextNodeMarker>();
const viewRefs = appRef._views;
for (const viewRef of viewRefs) {
const lView = getComponentLViewForHydration(viewRef);
const lNode = getLNodeForHydration(viewRef);

// An `lView` might be `null` if a `ViewRef` represents
// an embedded view (not a component view).
if (lView !== null) {
const hostElement = lView[HOST];
// Root elements might also be annotated with the `ngSkipHydration` attribute,
// check if it's present before starting the serialization process.
if (hostElement && !(hostElement as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
const context: HydrationContext = {
serializedViewCollection,
corruptedTextNodes,
};
annotateHostElementForHydration(hostElement as HTMLElement, lView, context);
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
if (lNode !== null) {
const context: HydrationContext = {
serializedViewCollection,
corruptedTextNodes,
};
if (isLContainer(lNode)) {
annotateLContainerForHydration(lNode, context);
} else {
annotateComponentLViewForHydration(lNode, context);
}
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
}
}

Expand Down Expand Up @@ -408,9 +457,11 @@ function componentUsesShadowDomEncapsulation(lView: LView): boolean {
* @param element The Host element to be annotated
* @param lView The associated LView
* @param context The hydration context
* @returns An index of serialized view from the transfer state object
* or `null` when a given component can not be serialized.
*/
function annotateHostElementForHydration(
element: RElement, lView: LView, context: HydrationContext): void {
element: RElement, lView: LView, context: HydrationContext): number|null {
const renderer = lView[RENDERER];
if (hasI18n(lView) || componentUsesShadowDomEncapsulation(lView)) {
// Attach the skip hydration attribute if this component:
Expand All @@ -419,10 +470,12 @@ function annotateHostElementForHydration(
// shadow DOM, so we can not guarantee that client and server representations
// would exactly match
renderer.setAttribute(element, SKIP_HYDRATION_ATTR_NAME, '');
return null;
} else {
const ngh = serializeLView(lView, context);
const index = context.serializedViewCollection.add(ngh);
renderer.setAttribute(element, NGH_ATTR_NAME, index.toString());
return index;
}
}

Expand Down
19 changes: 14 additions & 5 deletions packages/core/src/hydration/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import {ApplicationRef} from '../application_ref';
import {CONTAINER_HEADER_OFFSET, DEHYDRATED_VIEWS, LContainer} from '../render3/interfaces/container';
import {Renderer} from '../render3/interfaces/renderer';
import {RNode} from '../render3/interfaces/renderer_dom';
import {isLContainer} from '../render3/interfaces/type_checks';
import {isLContainer, isLView} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TVIEW} from '../render3/interfaces/view';
import {nativeRemoveNode} from '../render3/node_manipulation';
import {EMPTY_ARRAY} from '../util/empty';

import {validateSiblingNodeExists} from './error_handling';
import {DehydratedContainerView, NUM_ROOT_NODES} from './interfaces';
import {getComponentLViewForHydration} from './utils';
import {getLNodeForHydration} from './utils';

/**
* Removes all dehydrated views from a given LContainer:
Expand Down Expand Up @@ -92,11 +92,20 @@ function cleanupLView(lView: LView) {
export function cleanupDehydratedViews(appRef: ApplicationRef) {
const viewRefs = appRef._views;
for (const viewRef of viewRefs) {
const lView = getComponentLViewForHydration(viewRef);
const lNode = getLNodeForHydration(viewRef);
// An `lView` might be `null` if a `ViewRef` represents
// an embedded view (not a component view).
if (lView !== null && lView[HOST] !== null) {
cleanupLView(lView);
if (lNode !== null && lNode[HOST] !== null) {
if (isLView(lNode)) {
cleanupLView(lNode);
} else {
// Cleanup in the root component view
const componentLView = lNode[HOST] as LView<unknown>;
cleanupLView(componentLView);

// Cleanup in all views within this view container
cleanupLContainer(lNode);
}
ngDevMode && ngDevMode.dehydratedViewsCleanupRuns++;
}
}
Expand Down
79 changes: 59 additions & 20 deletions packages/core/src/hydration/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

import {Injector} from '../di/injector';
import {ViewRef} from '../linker/view_ref';
import {LContainer} from '../render3/interfaces/container';
import {getDocument} from '../render3/interfaces/document';
import {RElement, RNode} from '../render3/interfaces/renderer_dom';
import {isLContainer, isRootView} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, HOST, LView, TVIEW, TViewType} from '../render3/interfaces/view';
import {isRootView} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, LView, TVIEW, TViewType} from '../render3/interfaces/view';
import {makeStateKey, TransferState} from '../transfer_state';
import {assertDefined} from '../util/assert';

Expand Down Expand Up @@ -69,15 +70,34 @@ export const enum TextNodeMarker {
*
* @param rNode Component's host element.
* @param injector Injector that this component has access to.
* @param isRootView Specifies whether we trying to read hydration info for the root view.
*/
let _retrieveHydrationInfoImpl: typeof retrieveHydrationInfoImpl =
(rNode: RElement, injector: Injector) => null;
(rNode: RElement, injector: Injector, isRootView?: boolean) => null;

export function retrieveHydrationInfoImpl(rNode: RElement, injector: Injector): DehydratedView|
null {
const nghAttrValue = rNode.getAttribute(NGH_ATTR_NAME);
export function retrieveHydrationInfoImpl(
rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null {
let nghAttrValue = rNode.getAttribute(NGH_ATTR_NAME);
if (nghAttrValue == null) return null;

// For cases when a root component also acts as an anchor node for a ViewContainerRef
// (for example, when ViewContainerRef is injected in a root component), there is a need
// to serialize information about the component itself, as well as an LContainer that
// represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info:
// (1) hydration info for the root component itself and (2) hydration info for the
// ViewContainerRef instance (an LContainer). Each piece of information is included into
// the hydration data (in the TransferState object) separately, thus we end up with 2 ids.
// Since we only have 1 root element, we encode both bits of info into a single string:
// ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view
// and 25 is the `ngh` for a root view which holds LContainer).
const [componentViewNgh, rootViewNgh] = nghAttrValue.split('|');
nghAttrValue = isRootView ? rootViewNgh : componentViewNgh;
if (!nghAttrValue) return null;

// We've read one of the ngh ids, keep the remaining one, so that
// we can set it back on the DOM element.
const remainingNgh = isRootView ? componentViewNgh : (rootViewNgh ? `|${rootViewNgh}` : '');

let data: SerializedView = {};
// An element might have an empty `ngh` attribute value (e.g. `<comp ngh="" />`),
// which means that no special annotations are required. Do not attempt to read
Expand All @@ -101,9 +121,31 @@ export function retrieveHydrationInfoImpl(rNode: RElement, injector: Injector):
data,
firstChild: rNode.firstChild ?? null,
};
// The `ngh` attribute is cleared from the DOM node now
// that the data has been retrieved.
rNode.removeAttribute(NGH_ATTR_NAME);

if (isRootView) {
// If there is hydration info present for the root view, it means that there was
// a ViewContainerRef injected in the root component. The root component host element
// acted as an anchor node in this scenario. As a result, the DOM nodes that represent
// embedded views in this ViewContainerRef are located as siblings to the host node,
// i.e. `<app-root /><#VIEW1><#VIEW2>...<!--container-->`. In this case, the current
// node becomes the first child of this root view and the next sibling is the first
// element in the DOM segment.
dehydratedView.firstChild = rNode;

// We use `0` here, since this is the slot (right after the HEADER_OFFSET)
// where a component LView or an LContainer is located in a root LView.
setSegmentHead(dehydratedView, 0, rNode.nextSibling);
}

if (remainingNgh) {
// If we have only used one of the ngh ids, store the remaining one
// back on this RNode.
rNode.setAttribute(NGH_ATTR_NAME, remainingNgh);
} else {
// The `ngh` attribute is cleared from the DOM node now
// that the data has been retrieved for all indices.
rNode.removeAttribute(NGH_ATTR_NAME);
}

// Note: don't check whether this node was claimed for hydration,
// because this node might've been previously claimed while processing
Expand All @@ -125,15 +167,18 @@ export function enableRetrieveHydrationInfoImpl() {
* Retrieves hydration info by reading the value from the `ngh` attribute
* and accessing a corresponding slot in TransferState storage.
*/
export function retrieveHydrationInfo(rNode: RElement, injector: Injector): DehydratedView|null {
return _retrieveHydrationInfoImpl(rNode, injector);
export function retrieveHydrationInfo(
rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null {
return _retrieveHydrationInfoImpl(rNode, injector, isRootView);
}

/**
* Retrieves an instance of a component LView from a given ViewRef.
* Returns an instance of a component LView or `null` in case of an embedded view.
* Retrieves the necessary object from a given ViewRef to serialize:
* - an LView for component views
* - an LContainer for cases when component acts as a ViewContainerRef anchor
* - `null` in case of an embedded view
*/
export function getComponentLViewForHydration(viewRef: ViewRef): LView|null {
export function getLNodeForHydration(viewRef: ViewRef): LView|LContainer|null {
// Reading an internal field from `ViewRef` instance.
let lView = (viewRef as any)._lView as LView;
const tView = lView[TVIEW];
Expand All @@ -148,12 +193,6 @@ export function getComponentLViewForHydration(viewRef: ViewRef): LView|null {
lView = lView[HEADER_OFFSET];
}

// If a `ViewContainerRef` was injected in a component class, this resulted
// in an LContainer creation at that location. In this case, the component
// LView is in the LContainer's `HOST` slot.
if (isLContainer(lView)) {
lView = lView[HOST];
}
return lView;
}

Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/render3/component_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,17 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
LViewFlags.CheckAlways | LViewFlags.IsRoot;
const rootFlags = this.componentDef.signals ? signalFlags : nonSignalFlags;

let hydrationInfo: DehydratedView|null = null;
if (hostRNode !== null) {
hydrationInfo = retrieveHydrationInfo(hostRNode, rootViewInjector, true /* isRootView */);
}

// Create the root view. Uses empty TView and ContentTemplate.
const rootTView =
createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null, null);
const rootLView = createLView(
null, rootTView, null, rootFlags, null, null, environment, hostRenderer, rootViewInjector,
null, null);
null, hydrationInfo);

// rootView is the parent when bootstrapping
// TODO(misko): it looks like we are entering view here but we don't really need to as
Expand Down Expand Up @@ -382,7 +387,7 @@ function createRootComponentView(
const tView = rootView[TVIEW];
applyRootComponentStyling(rootDirectives, tNode, hostRNode, hostRenderer);

// Hydration info is on the host element and needs to be retreived
// Hydration info is on the host element and needs to be retrieved
// and passed to the component LView.
let hydrationInfo: DehydratedView|null = null;
if (hostRNode !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -788,9 +788,6 @@
{
"name": "getComponentLViewByIndex"
},
{
"name": "getComponentLViewForHydration"
},
{
"name": "getCurrentTNode"
},
Expand Down Expand Up @@ -827,6 +824,9 @@
{
"name": "getInjectorIndex"
},
{
"name": "getLNodeForHydration"
},
{
"name": "getLView"
},
Expand Down Expand Up @@ -1235,6 +1235,9 @@
{
"name": "setInjectImplementation"
},
{
"name": "setSegmentHead"
},
{
"name": "setSelectedIndex"
},
Expand Down
Loading

0 comments on commit 47e33d2

Please sign in to comment.