Skip to content

Commit f751ce6

Browse files
AndrewKushniralxhub
authored andcommitted
fix(core): handle projection of hydrated containters into components that skip hydration (#50199)
This commit updates hydration logic to support a scenario where a view container that was hydrated and later on projected to a component that skips hydration. Currently, such projected content is extracted from the DOM (since a component that skips hydration needs to be re-created), but never added back, since the current logic treats such content as "already inserted". Closes #50175. PR Close #50199
1 parent bb8fb10 commit f751ce6

File tree

12 files changed

+328
-11
lines changed

12 files changed

+328
-11
lines changed

packages/core/src/hydration/annotate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {TransferState} from '../transfer_state';
2121
import {unsupportedProjectionOfDomNodes} from './error_handling';
2222
import {CONTAINERS, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NODES, NUM_ROOT_NODES, SerializedContainerView, SerializedView, TEMPLATE_ID, TEMPLATES} from './interfaces';
2323
import {calcPathForNode} from './node_lookup_utils';
24-
import {isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
24+
import {hasInSkipHydrationBlockFlag, isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
2525
import {getComponentLViewForHydration, NGH_ATTR_NAME, NGH_DATA_KEY, TextNodeMarker} from './utils';
2626

2727
/**

packages/core/src/hydration/skip_hydration.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {TNode} from '../render3/interfaces/node';
9+
import {TNode, TNodeFlags} from '../render3/interfaces/node';
1010
import {LView} from '../render3/interfaces/view';
1111

1212
/**
@@ -36,10 +36,22 @@ export function hasNgSkipHydrationAttr(tNode: TNode): boolean {
3636
return false;
3737
}
3838

39+
/**
40+
* Checks whether a TNode has a flag to indicate that it's a part of
41+
* a skip hydration block.
42+
*/
43+
export function hasInSkipHydrationBlockFlag(tNode: TNode): boolean {
44+
return (tNode.flags & TNodeFlags.inSkipHydrationBlock) === TNodeFlags.inSkipHydrationBlock;
45+
}
46+
3947
/**
4048
* Helper function that determines if a given node is within a skip hydration block
4149
* by navigating up the TNode tree to see if any parent nodes have skip hydration
4250
* attribute.
51+
*
52+
* TODO(akushnir): this function should contain the logic of `hasInSkipHydrationBlockFlag`,
53+
* there is no need to traverse parent nodes when we have a TNode flag (which would also
54+
* make this lookup O(1)).
4355
*/
4456
export function isInSkipHydrationBlock(tNode: TNode): boolean {
4557
let currentTNode: TNode|null = tNode.parent;

packages/core/src/linker/view_container_ref.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Injector} from '../di/injector';
1010
import {EnvironmentInjector} from '../di/r3_injector';
1111
import {validateMatchingNode} from '../hydration/error_handling';
1212
import {CONTAINERS} from '../hydration/interfaces';
13-
import {isInSkipHydrationBlock} from '../hydration/skip_hydration';
13+
import {hasInSkipHydrationBlockFlag, isInSkipHydrationBlock} from '../hydration/skip_hydration';
1414
import {getSegmentHead, isDisconnectedNode, markRNodeAsClaimedByHydration} from '../hydration/utils';
1515
import {findMatchingDehydratedView, locateDehydratedViewsInContainer} from '../hydration/views';
1616
import {isType, Type} from '../interface/type';
@@ -311,7 +311,11 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
311311

312312
const hydrationInfo = findMatchingDehydratedView(this._lContainer, templateRef.ssrId);
313313
const viewRef = templateRef.createEmbeddedViewImpl(context || <any>{}, injector, hydrationInfo);
314-
this.insertImpl(viewRef, index, !!hydrationInfo);
314+
// If there is a matching dehydrated view, but the host TNode is located in the skip
315+
// hydration block, this means that the content was detached (as a part of the skip
316+
// hydration logic) and it needs to be appended into the DOM.
317+
const skipDomInsertion = !!hydrationInfo && !hasInSkipHydrationBlockFlag(this._hostTNode);
318+
this.insertImpl(viewRef, index, skipDomInsertion);
315319
return viewRef;
316320
}
317321

@@ -428,7 +432,11 @@ const R3ViewContainerRef = class ViewContainerRef extends VE_ViewContainerRef {
428432
const rNode = dehydratedView?.firstChild ?? null;
429433
const componentRef =
430434
componentFactory.create(contextInjector, projectableNodes, rNode, environmentInjector);
431-
this.insertImpl(componentRef.hostView, index, !!dehydratedView);
435+
// If there is a matching dehydrated view, but the host TNode is located in the skip
436+
// hydration block, this means that the content was detached (as a part of the skip
437+
// hydration logic) and it needs to be appended into the DOM.
438+
const skipDomInsertion = !!dehydratedView && !hasInSkipHydrationBlockFlag(this._hostTNode);
439+
this.insertImpl(componentRef.hostView, index, skipDomInsertion);
432440
return componentRef;
433441
}
434442

@@ -638,8 +646,13 @@ function locateOrCreateAnchorNode(
638646

639647
const hydrationInfo = hostLView[HYDRATION];
640648
const noOffsetIndex = hostTNode.index - HEADER_OFFSET;
641-
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock(hostTNode) ||
642-
isDisconnectedNode(hydrationInfo, noOffsetIndex);
649+
650+
// TODO(akushnir): this should really be a single condition, refactor the code
651+
// to use `hasInSkipHydrationBlockFlag` logic inside `isInSkipHydrationBlock`.
652+
const skipHydration = isInSkipHydrationBlock(hostTNode) || hasInSkipHydrationBlockFlag(hostTNode);
653+
654+
const isNodeCreationMode =
655+
!hydrationInfo || skipHydration || isDisconnectedNode(hydrationInfo, noOffsetIndex);
643656

644657
// Regular creation mode.
645658
if (isNodeCreationMode) {

packages/core/src/render3/instructions/shared.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Injector} from '../../di/injector';
1010
import {ErrorHandler} from '../../error_handler';
1111
import {RuntimeError, RuntimeErrorCode} from '../../errors';
1212
import {DehydratedView} from '../../hydration/interfaces';
13-
import {SKIP_HYDRATION_ATTR_NAME} from '../../hydration/skip_hydration';
13+
import {hasInSkipHydrationBlockFlag, SKIP_HYDRATION_ATTR_NAME} from '../../hydration/skip_hydration';
1414
import {PRESERVE_HOST_CONTENT, PRESERVE_HOST_CONTENT_DEFAULT} from '../../hydration/tokens';
1515
import {processTextNodeMarkersBeforeHydration} from '../../hydration/utils';
1616
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
@@ -42,7 +42,7 @@ import {clearElementContents, updateTextNode} from '../node_manipulation';
4242
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
4343
import {profiler, ProfilerEvent} from '../profiler';
4444
import {commitLViewConsumerIfHasProducers, getReactiveLViewConsumer} from '../reactive_lview_consumer';
45-
import {getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, leaveView, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
45+
import {getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, isInSkipHydrationBlock, leaveView, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
4646
import {NO_CHANGE} from '../tokens';
4747
import {mergeHostAttrs} from '../util/attrs_utils';
4848
import {INTERPOLATION_DELIMITER} from '../util/misc_utils';
@@ -584,6 +584,10 @@ export function createTNode(
584584
ngDevMode && ngDevMode.tNode++;
585585
ngDevMode && tParent && assertTNodeForTView(tParent, tView);
586586
let injectorIndex = tParent ? tParent.injectorIndex : -1;
587+
let flags = 0;
588+
if (isInSkipHydrationBlock()) {
589+
flags |= TNodeFlags.inSkipHydrationBlock;
590+
}
587591
const tNode = {
588592
type,
589593
index,
@@ -594,7 +598,7 @@ export function createTNode(
594598
directiveStylingLast: -1,
595599
componentOffset: -1,
596600
propertyBindings: null,
597-
flags: 0,
601+
flags,
598602
providerIndexes: 0,
599603
value: value,
600604
attrs: attrs,

packages/core/src/render3/interfaces/node.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export const enum TNodeFlags {
115115
/** Bit #5 - This bit is set if the node has any "style" inputs */
116116
hasStyleInput = 0x10,
117117

118-
/** Bit #6 This bit is set if the node has been detached by i18n */
118+
/** Bit #6 - This bit is set if the node has been detached by i18n */
119119
isDetached = 0x20,
120120

121121
/**
@@ -125,6 +125,11 @@ export const enum TNodeFlags {
125125
* that actually have directives with host bindings.
126126
*/
127127
hasHostBindings = 0x40,
128+
129+
/**
130+
* Bit #8 - This bit is set if the node is a located inside skip hydration block.
131+
*/
132+
inSkipHydrationBlock = 0x80,
128133
}
129134

130135
/**

packages/core/src/render3/node_manipulation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {hasInSkipHydrationBlockFlag} from '../hydration/skip_hydration';
910
import {ViewEncapsulation} from '../metadata/view';
1011
import {RendererStyleFlags2} from '../render/api_flags';
1112
import {addToArray, removeFromArray} from '../util/array_utils';
@@ -964,6 +965,11 @@ function applyProjectionRecursive(
964965
} else {
965966
let nodeToProject: TNode|null = nodeToProjectOrRNodes;
966967
const projectedComponentLView = componentLView[PARENT] as LView;
968+
// If a parent <ng-content> is located within a skip hydration block,
969+
// annotate an actual node that is being projected with the same flag too.
970+
if (hasInSkipHydrationBlockFlag(tProjectionNode)) {
971+
nodeToProject.flags |= TNodeFlags.inSkipHydrationBlock;
972+
}
967973
applyNodes(
968974
renderer, action, nodeToProject, projectedComponentLView, parentRElement, beforeNode, true);
969975
}

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,9 @@
11451145
{
11461146
"name": "handleError"
11471147
},
1148+
{
1149+
"name": "hasInSkipHydrationBlockFlag"
1150+
},
11481151
{
11491152
"name": "hasParentInjector"
11501153
},

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,9 @@
11091109
{
11101110
"name": "handleError"
11111111
},
1112+
{
1113+
"name": "hasInSkipHydrationBlockFlag"
1114+
},
11121115
{
11131116
"name": "hasParentInjector"
11141117
},

packages/core/test/bundling/hydration/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,9 @@
905905
{
906906
"name": "getTNodeFromLView"
907907
},
908+
{
909+
"name": "hasInSkipHydrationBlockFlag"
910+
},
908911
{
909912
"name": "hostReportError"
910913
},

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,9 @@
14421442
{
14431443
"name": "hasEmptyPathConfig"
14441444
},
1445+
{
1446+
"name": "hasInSkipHydrationBlockFlag"
1447+
},
14451448
{
14461449
"name": "hasParentInjector"
14471450
},

0 commit comments

Comments
 (0)