angular/packages/core/src/hydration/annotate.ts
Joey Perrott 9dbe6fc18b refactor: update license text to point to angular.dev (#57901)
Update license text to point to angular.dev instead of angular.io

PR Close #57901
2024-09-24 15:33:00 +02:00

699 lines
27 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {ApplicationRef} from '../application/application_ref';
import {isDetachedByI18n} from '../i18n/utils';
import {ViewEncapsulation} from '../metadata';
import {Renderer2} from '../render';
import {assertTNode} from '../render3/assert';
import {collectNativeNodes, collectNativeNodesInLContainer} from '../render3/collect_native_nodes';
import {getComponentDef} from '../render3/definition';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
import {isLetDeclaration, isTNodeShape, 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 {unwrapLView, unwrapRNode} from '../render3/util/view_utils';
import {TransferState} from '../transfer_state';
import {unsupportedProjectionOfDomNodes} from './error_handling';
import {collectDomEventsInfo} from './event_replay';
import {setJSActionAttributes} from '../event_delegation_utils';
import {
getOrComputeI18nChildren,
isI18nHydrationEnabled,
isI18nHydrationSupportEnabled,
trySerializeI18nBlock,
} from './i18n';
import {
CONTAINERS,
DISCONNECTED_NODES,
ELEMENT_CONTAINERS,
I18N_DATA,
MULTIPLIER,
NODES,
NUM_ROOT_NODES,
SerializedContainerView,
SerializedView,
TEMPLATE_ID,
TEMPLATES,
} from './interfaces';
import {calcPathForNode, isDisconnectedNode} from './node_lookup_utils';
import {isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
import {EVENT_REPLAY_ENABLED_DEFAULT, IS_EVENT_REPLAY_ENABLED} from './tokens';
import {
getLNodeForHydration,
NGH_ATTR_NAME,
NGH_DATA_KEY,
processTextNodeBeforeSerialization,
TextNodeMarker,
} from './utils';
/**
* A collection that tracks all serialized views (`ngh` DOM annotations)
* to avoid duplication. An attempt to add a duplicate view results in the
* collection returning the index of the previously collected serialized view.
* This reduces the number of annotations needed for a given page.
*/
class SerializedViewCollection {
private views: SerializedView[] = [];
private indexByContent = new Map<string, number>();
add(serializedView: SerializedView): number {
const viewAsString = JSON.stringify(serializedView);
if (!this.indexByContent.has(viewAsString)) {
const index = this.views.length;
this.views.push(serializedView);
this.indexByContent.set(viewAsString, index);
return index;
}
return this.indexByContent.get(viewAsString)!;
}
getAll(): SerializedView[] {
return this.views;
}
}
/**
* Global counter that is used to generate a unique id for TViews
* during the serialization process.
*/
let tViewSsrId = 0;
/**
* Generates a unique id for a given TView and returns this id.
* The id is also stored on this instance of a TView and reused in
* subsequent calls.
*
* This id is needed to uniquely identify and pick up dehydrated views
* at runtime.
*/
function getSsrId(tView: TView): string {
if (!tView.ssrId) {
tView.ssrId = `t${tViewSsrId++}`;
}
return tView.ssrId;
}
/**
* Describes a context available during the serialization
* process. The context is used to share and collect information
* during the serialization.
*/
export interface HydrationContext {
serializedViewCollection: SerializedViewCollection;
corruptedTextNodes: Map<HTMLElement, TextNodeMarker>;
isI18nHydrationEnabled: boolean;
i18nChildren: Map<TView, Set<number> | null>;
eventTypesToReplay: {regular: Set<string>; capture: Set<string>};
shouldReplayEvents: boolean;
}
/**
* Computes the number of root nodes in a given view
* (or child nodes in a given container if a tNode is provided).
*/
function calcNumRootNodes(tView: TView, lView: LView, tNode: TNode | null): number {
const rootNodes: unknown[] = [];
collectNativeNodes(tView, lView, tNode, rootNodes);
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.
*/
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 = unwrapLView(lContainer[HOST]) as LView<unknown>;
// Serialize the root component itself.
const componentLViewNghIndex = annotateComponentLViewForHydration(componentLView, context);
if (componentLViewNghIndex === null) {
// Component was not serialized (for example, if hydration was skipped by adding
// the `ngSkipHydration` attribute or this component uses i18n blocks in the template,
// but `withI18nSupport()` was not added), avoid annotating host element with the `ngh`
// attribute.
return;
}
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.
*
* @param appRef An instance of an ApplicationRef.
* @param doc A reference to the current Document instance.
* @return event types that need to be replayed
*/
export function annotateForHydration(appRef: ApplicationRef, doc: Document) {
const injector = appRef.injector;
const isI18nHydrationEnabledVal = isI18nHydrationEnabled(injector);
const serializedViewCollection = new SerializedViewCollection();
const corruptedTextNodes = new Map<HTMLElement, TextNodeMarker>();
const viewRefs = appRef._views;
const shouldReplayEvents = injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT);
const eventTypesToReplay = {
regular: new Set<string>(),
capture: new Set<string>(),
};
for (const viewRef of viewRefs) {
const lNode = getLNodeForHydration(viewRef);
// An `lView` might be `null` if a `ViewRef` represents
// an embedded view (not a component view).
if (lNode !== null) {
const context: HydrationContext = {
serializedViewCollection,
corruptedTextNodes,
isI18nHydrationEnabled: isI18nHydrationEnabledVal,
i18nChildren: new Map(),
eventTypesToReplay,
shouldReplayEvents,
};
if (isLContainer(lNode)) {
annotateLContainerForHydration(lNode, context);
} else {
annotateComponentLViewForHydration(lNode, context);
}
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
}
}
// Note: we *always* include hydration info key and a corresponding value
// into the TransferState, even if the list of serialized views is empty.
// This is needed as a signal to the client that the server part of the
// hydration logic was setup and enabled correctly. Otherwise, if a client
// hydration doesn't find a key in the transfer state - an error is produced.
const serializedViews = serializedViewCollection.getAll();
const transferState = injector.get(TransferState);
transferState.set(NGH_DATA_KEY, serializedViews);
return eventTypesToReplay;
}
/**
* Serializes the lContainer data into a list of SerializedView objects,
* that represent views within this lContainer.
*
* @param lContainer the lContainer we are serializing
* @param context the hydration context
* @returns an array of the `SerializedView` objects
*/
function serializeLContainer(
lContainer: LContainer,
context: HydrationContext,
): SerializedContainerView[] {
const views: SerializedContainerView[] = [];
let lastViewAsString = '';
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
let childLView = lContainer[i] as LView;
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,
};
}
}
if (!serializedView) {
const childTView = childLView[TVIEW];
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);
}
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(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(serializedView);
}
}
return views;
}
/**
* Helper function to produce a node path (which navigation steps runtime logic
* needs to take to locate a node) and stores it in the `NODES` section of the
* current serialized view.
*/
function appendSerializedNodePath(
ngh: SerializedView,
tNode: TNode,
lView: LView,
excludedParentNodes: Set<number> | null,
) {
const noOffsetIndex = tNode.index - HEADER_OFFSET;
ngh[NODES] ??= {};
// Ensure we don't calculate the path multiple times.
ngh[NODES][noOffsetIndex] ??= calcPathForNode(tNode, lView, excludedParentNodes);
}
/**
* Helper function to append information about a disconnected node.
* This info is needed at runtime to avoid DOM lookups for this element
* and instead, the element would be created from scratch.
*/
function appendDisconnectedNodeIndex(ngh: SerializedView, tNodeOrNoOffsetIndex: TNode | number) {
const noOffsetIndex =
typeof tNodeOrNoOffsetIndex === 'number'
? tNodeOrNoOffsetIndex
: tNodeOrNoOffsetIndex.index - HEADER_OFFSET;
ngh[DISCONNECTED_NODES] ??= [];
if (!ngh[DISCONNECTED_NODES].includes(noOffsetIndex)) {
ngh[DISCONNECTED_NODES].push(noOffsetIndex);
}
}
/**
* Serializes the lView data into a SerializedView object that will later be added
* to the TransferState storage and referenced using the `ngh` attribute on a host
* element.
*
* @param lView the lView we are serializing
* @param context the hydration context
* @returns the `SerializedView` object containing the data to be added to the host node
*/
function serializeLView(lView: LView, context: HydrationContext): SerializedView {
const ngh: SerializedView = {};
const tView = lView[TVIEW];
const i18nChildren = getOrComputeI18nChildren(tView, context);
const nativeElementsToEventTypes = context.shouldReplayEvents
? collectDomEventsInfo(tView, lView, context.eventTypesToReplay)
: null;
// Iterate over DOM element references in an LView.
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
const tNode = tView.data[i];
const noOffsetIndex = i - HEADER_OFFSET;
// Attempt to serialize any i18n data for the given slot. We do this first, as i18n
// has its own process for serialization.
const i18nData = trySerializeI18nBlock(lView, i, context);
if (i18nData) {
ngh[I18N_DATA] ??= {};
ngh[I18N_DATA][noOffsetIndex] = i18nData.caseQueue;
for (const nodeNoOffsetIndex of i18nData.disconnectedNodes) {
appendDisconnectedNodeIndex(ngh, nodeNoOffsetIndex);
}
for (const nodeNoOffsetIndex of i18nData.disjointNodes) {
const tNode = tView.data[nodeNoOffsetIndex + HEADER_OFFSET] as TNode;
ngDevMode && assertTNode(tNode);
appendSerializedNodePath(ngh, tNode, lView, i18nChildren);
}
continue;
}
// Skip processing of a given slot in the following cases:
// - Local refs (e.g. <div #localRef>) take up an extra slot in LViews
// to store the same element. In this case, there is no information in
// a corresponding slot in TNode data structure.
// - When a slot contains something other than a TNode. For example, there
// might be some metadata information about a defer block or a control flow block.
if (!isTNodeShape(tNode)) {
continue;
}
// Skip any nodes that are in an i18n block but are considered detached (i.e. not
// present in the template). These nodes are disconnected from the DOM tree, and
// so we don't want to serialize any information about them.
if (isDetachedByI18n(tNode)) {
continue;
}
// Check if a native node that represents a given TNode is disconnected from the DOM tree.
// Such nodes must be excluded from the hydration (since the hydration won't be able to
// find them), so the TNode ids are collected and used at runtime to skip the hydration.
//
// This situation may happen during the content projection, when some nodes don't make it
// into one of the content projection slots (for example, when there is no default
// <ng-content /> slot in projector component's template).
if (isDisconnectedNode(tNode, lView) && isContentProjectedNode(tNode)) {
appendDisconnectedNodeIndex(ngh, tNode);
continue;
}
// Attach `jsaction` attribute to elements that have registered listeners,
// thus potentially having a need to do an event replay.
if (nativeElementsToEventTypes && tNode.type & TNodeType.Element) {
const nativeElement = unwrapRNode(lView[i]) as Element;
if (nativeElementsToEventTypes.has(nativeElement)) {
setJSActionAttributes(nativeElement, nativeElementsToEventTypes.get(nativeElement)!);
}
}
if (Array.isArray(tNode.projection)) {
for (const projectionHeadTNode of tNode.projection) {
// We may have `null`s in slots with no projected content.
if (!projectionHeadTNode) continue;
if (!Array.isArray(projectionHeadTNode)) {
// If we process re-projected content (i.e. `<ng-content>`
// appears at projection location), skip annotations for this content
// since all DOM nodes in this projection were handled while processing
// a parent lView, which contains those nodes.
if (
!isProjectionTNode(projectionHeadTNode) &&
!isInSkipHydrationBlock(projectionHeadTNode)
) {
if (isDisconnectedNode(projectionHeadTNode, lView)) {
// Check whether this node is connected, since we may have a TNode
// in the data structure as a projection segment head, but the
// content projection slot might be disabled (e.g.
// <ng-content *ngIf="false" />).
appendDisconnectedNodeIndex(ngh, projectionHeadTNode);
} else {
appendSerializedNodePath(ngh, projectionHeadTNode, lView, i18nChildren);
}
}
} else {
// If a value is an array, it means that we are processing a projection
// where projectable nodes were passed in as DOM nodes (for example, when
// calling `ViewContainerRef.createComponent(CmpA, {projectableNodes: [...]})`).
//
// In this scenario, nodes can come from anywhere (either created manually,
// accessed via `document.querySelector`, etc) and may be in any state
// (attached or detached from the DOM tree). As a result, we can not reliably
// restore the state for such cases during hydration.
throw unsupportedProjectionOfDomNodes(unwrapRNode(lView[i]));
}
}
}
conditionallyAnnotateNodePath(ngh, tNode, lView, i18nChildren);
if (isLContainer(lView[i])) {
// Serialize information about a template.
const embeddedTView = tNode.tView;
if (embeddedTView !== null) {
ngh[TEMPLATES] ??= {};
ngh[TEMPLATES][noOffsetIndex] = getSsrId(embeddedTView);
}
// Serialize views within this LContainer.
const hostNode = lView[i][HOST]!; // host node of this container
// LView[i][HOST] can be of 2 different types:
// - either a DOM node
// - or an array that represents an LView of a component
if (Array.isArray(hostNode)) {
// This is a component, serialize info about it.
const targetNode = unwrapRNode(hostNode as LView) as RElement;
if (!(targetNode as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
annotateHostElementForHydration(targetNode, hostNode as LView, context);
}
}
ngh[CONTAINERS] ??= {};
ngh[CONTAINERS][noOffsetIndex] = serializeLContainer(lView[i], context);
} else if (Array.isArray(lView[i]) && !isLetDeclaration(tNode)) {
// This is a component, annotate the host node with an `ngh` attribute.
// Note: Let declarations that return an array are also storing an array in the LView,
// we need to exclude them.
const targetNode = unwrapRNode(lView[i][HOST]!);
if (!(targetNode as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
annotateHostElementForHydration(targetNode as RElement, lView[i], context);
}
} else {
// <ng-container> case
if (tNode.type & TNodeType.ElementContainer) {
// An <ng-container> is represented by the number of
// top-level nodes. This information is needed to skip over
// those nodes to reach a corresponding anchor node (comment node).
ngh[ELEMENT_CONTAINERS] ??= {};
ngh[ELEMENT_CONTAINERS][noOffsetIndex] = calcNumRootNodes(tView, lView, tNode.child);
} else if (tNode.type & (TNodeType.Projection | TNodeType.LetDeclaration)) {
// Current TNode represents an `<ng-content>` slot or `@let` declaration,
// thus it has no DOM elements associated with it, so the **next sibling**
// node would not be able to find an anchor. In this case, use full path instead.
let nextTNode = tNode.next;
// Skip over all `<ng-content>` slots and `@let` declarations in a row.
while (
nextTNode !== null &&
nextTNode.type & (TNodeType.Projection | TNodeType.LetDeclaration)
) {
nextTNode = nextTNode.next;
}
if (nextTNode && !isInSkipHydrationBlock(nextTNode)) {
// Handle a tNode after the `<ng-content>` slot.
appendSerializedNodePath(ngh, nextTNode, lView, i18nChildren);
}
} else if (tNode.type & TNodeType.Text) {
const rNode = unwrapRNode(lView[i]);
processTextNodeBeforeSerialization(context, rNode);
}
}
}
return ngh;
}
/**
* Serializes node location in cases when it's needed, specifically:
*
* 1. If `tNode.projectionNext` is different from `tNode.next` - it means that
* the next `tNode` after projection is different from the one in the original
* template. Since hydration relies on `tNode.next`, this serialized info
* is required to help runtime code find the node at the correct location.
* 2. In certain content projection-based use-cases, it's possible that only
* a content of a projected element is rendered. In this case, content nodes
* require an extra annotation, since runtime logic can't rely on parent-child
* connection to identify the location of a node.
*/
function conditionallyAnnotateNodePath(
ngh: SerializedView,
tNode: TNode,
lView: LView<unknown>,
excludedParentNodes: Set<number> | null,
) {
if (isProjectionTNode(tNode)) {
// Do not annotate projection nodes (<ng-content />), since
// they don't have a corresponding DOM node representing them.
return;
}
// Handle case #1 described above.
if (
tNode.projectionNext &&
tNode.projectionNext !== tNode.next &&
!isInSkipHydrationBlock(tNode.projectionNext)
) {
appendSerializedNodePath(ngh, tNode.projectionNext, lView, excludedParentNodes);
}
// Handle case #2 described above.
// Note: we only do that for the first node (i.e. when `tNode.prev === null`),
// the rest of the nodes would rely on the current node location, so no extra
// annotation is needed.
if (
tNode.prev === null &&
tNode.parent !== null &&
isDisconnectedNode(tNode.parent, lView) &&
!isDisconnectedNode(tNode, lView)
) {
appendSerializedNodePath(ngh, tNode, lView, excludedParentNodes);
}
}
/**
* Determines whether a component instance that is represented
* by a given LView uses `ViewEncapsulation.ShadowDom`.
*/
function componentUsesShadowDomEncapsulation(lView: LView): boolean {
const instance = lView[CONTEXT];
return instance?.constructor
? getComponentDef(instance.constructor)?.encapsulation === ViewEncapsulation.ShadowDom
: false;
}
/**
* Annotates component host element for hydration:
* - by either adding the `ngh` attribute and collecting hydration-related info
* for the serialization and transferring to the client
* - or by adding the `ngSkipHydration` attribute in case Angular detects that
* component contents is not compatible with hydration.
*
* @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,
): number | null {
const renderer = lView[RENDERER];
if (
(hasI18n(lView) && !isI18nHydrationSupportEnabled()) ||
componentUsesShadowDomEncapsulation(lView)
) {
// Attach the skip hydration attribute if this component:
// - either has i18n blocks, since hydrating such blocks is not yet supported
// - or uses ShadowDom view encapsulation, since Domino doesn't support
// 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;
}
}
/**
* Physically inserts the comment nodes to ensure empty text nodes and adjacent
* text node separators are preserved after server serialization of the DOM.
* These get swapped back for empty text nodes or separators once hydration happens
* on the client.
*
* @param corruptedTextNodes The Map of text nodes to be replaced with comments
* @param doc The document
*/
function insertCorruptedTextNodeMarkers(
corruptedTextNodes: Map<HTMLElement, string>,
doc: Document,
) {
for (const [textNode, marker] of corruptedTextNodes) {
textNode.after(doc.createComment(marker));
}
}
/**
* Detects whether a given TNode represents a node that
* is being content projected.
*/
function isContentProjectedNode(tNode: TNode): boolean {
let currentTNode = tNode;
while (currentTNode != null) {
// If we come across a component host node in parent nodes -
// this TNode is in the content projection section.
if (isComponentHost(currentTNode)) {
return true;
}
currentTNode = currentTNode.parent as TNode;
}
return false;
}