Skip to content

Commit

Permalink
WIP: fix(core): support hydration of view containers for root components
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKushnir committed Aug 1, 2023
1 parent e4ae634 commit 7d9d315
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 50 deletions.
72 changes: 56 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 {hasI18n, isComponentHost, isLContainer, isLView, isProjectionTNode, isRootView} from '../render3/interfaces/type_checks';
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, 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 {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,43 @@ function calcNumRootNodes(tView: TView, lView: LView, tNode: TNode|null): number
return rootNodes.length;
}

// TODO: add docs.
function annotateLViewForHydration(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;
}

// TODO: add docs.
function annotateLContainerForHydration(lContainer: LContainer, context: HydrationContext) {
const componentLView = lContainer[HOST] as LView<unknown>;

// Serialize root component itself.
const componentLViewNghIndex = annotateLViewForHydration(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);

// TODO: assert whether we have `ngSkipHydration` on this hostElement and
// whether there are any views in this LContainer. If so, produce a console.warn
// to indicate that the DOM for those views would *not* be cleaned up, which
// might result in duplicate content on a page.

const renderer = componentLView[RENDERER] as Renderer2;

// Combine both indices into a single string and
// apply to the host node as an `ngh` attribute value.
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 +141,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 {
annotateLViewForHydration(lNode, context);
}
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
}
}

Expand Down Expand Up @@ -410,7 +448,7 @@ function componentUsesShadowDomEncapsulation(lView: LView): boolean {
* @param context The hydration context
*/
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 +457,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
14 changes: 9 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,15 @@ 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 {
cleanupLContainer(lNode);
}
ngDevMode && ngDevMode.dehydratedViewsCleanupRuns++;
}
}
Expand Down
74 changes: 54 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 @@ -64,15 +65,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 try 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 teh 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 @@ -96,9 +116,28 @@ 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;
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.
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 @@ -120,15 +159,16 @@ 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 an instance of a component LView or an LContainer from a given ViewRef.
* Returns an instance of a component LView or an LContainer, or `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 @@ -143,12 +183,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 @@ -212,12 +212,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 @@ -386,7 +391,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
Loading

0 comments on commit 7d9d315

Please sign in to comment.