8000 fix(core): Fixes template outlet hydration (#62012) · angular/angular@e343cdf · GitHub
[go: up one dir, main page]

Skip to content

Commit e343cdf

Browse files
fix(core): Fixes template outlet hydration (#62012)
Projected nodes were missing ssrId information and were skipping annotating template information, which caused templates to be destroyed and recreated rather than hydrated. fixes: #50543 PR Close #62012
1 parent 43ef2ea commit e343cdf

File tree

3 files changed

+59
-17
lines changed

3 files changed

+59
-17
lines changed

packages/core/src/hydration/annotate.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -595,10 +595,15 @@ function serializeLView(
595595
continue;
596596
}
597597

598+
// Serialize information about template.
599+
if (isLContainer(lView[i]) && tNode.tView) {
600+
ngh[TEMPLATES] ??= {};
601+
ngh[TEMPLATES][noOffsetIndex] = getSsrId(tNode.tView!);
602+
}
603+
598604
// Check if a native node that represents a given TNode is disconnected from the DOM tree.
599605
// Such nodes must be excluded from the hydration (since the hydration won't be able to
600606
// find them), so the TNode ids are collected and used at runtime to skip the hydration.
601-
//
602607
// This situation may happen during the content projection, when some nodes don't make it
603608
// into one of the content projection slots (for example, when there is no default
604609
// <ng-content /> slot in projector component's template).
@@ -648,13 +653,6 @@ function serializeLView(
648653

649654
conditionallyAnnotateNodePath(ngh, tNode, lView, i18nChildren);
650655
if (isLContainer(lView[i])) {
651-
// Serialize information about a template.
652-
const embeddedTView = tNode.tView;
653-
if (embeddedTView !== null) {
654-
ngh[TEMPLATES] ??= {};
655-
ngh[TEMPLATES][noOffsetIndex] = getSsrId(embeddedTView);
656-
}
657-
658656
// Serialize views within this LContainer.
659657
const hostNode = lView[i][HOST]!; // host node of this container
660658

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -264,19 +264,15 @@ function locateOrCreateContainerAnchorImpl(
264264
index: number,
265265
): RComment {
266266
const hydrationInfo = lView[HYDRATION];
267+
267268
const isNodeCreationMode =
268269
!hydrationInfo ||
269270
isInSkipHydrationBlock() ||
270271
isDetachedByI18n(tNode) ||
271272
isDisconnectedNode(hydrationInfo, index);
272273
lastNodeWasCreated(isNodeCreationMode);
273274

274-
// Regular creation mode.
275-
if (isNodeCreationMode) {
276-
return createContainerAnchorImpl(tView, lView, tNode, index);
277-
}
278-
279-
const ssrId = hydrationInfo.data[TEMPLATES]?.[index] ?? null;
275+
const ssrId = hydrationInfo?.data[TEMPLATES]?.[index] ?? null;
280276

281277
// Apply `ssrId` value to the underlying TView if it was not previously set.
282278
//
@@ -295,12 +291,17 @@ function locateOrCreateContainerAnchorImpl(
295291
}
296292
}
297293

294+
// Regular creation mode.
295+
if (isNodeCreationMode) {
296+
return createContainerAnchorImpl(tView, lView, tNode, index);
297+
}
298+
298299
// Hydration mode, looking up existing elements in DOM.
299-
const currentRNode = locateNextRNode(hydrationInfo, tView, lView, tNode)!;
300+
const currentRNode = locateNextRNode(hydrationInfo!, tView, lView, tNode)!;
300301
ngDevMode && validateNodeExists(currentRNode, lView, tNode);
301302

302-
setSegmentHead(hydrationInfo, index, currentRNode);
303-
const viewContainerSize = calcSerializedContainerSize(hydrationInfo, index);
303+
setSegmentHead(hydrationInfo!, index, currentRNode);
304+
const viewContainerSize = calcSerializedContainerSize(hydrationInfo!, index);
304305
const comment = siblingAfter<RComment>(viewContainerSize, currentRNode)!;
305306

306307
if (ngDevMode) {

packages/platform-server/test/full_app_hydration_spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
ViewContainerRef,
5252
ViewEncapsulation,
5353
ɵNoopNgZone as NoopNgZone,
54+
ContentChild,
5455
} from '@angular/core';
5556
import {TestBed} from '@angular/core/testing';
5657
import {clearTranslations, loadTranslations} from '@angular/localize';
@@ -5940,6 +5941,48 @@ describe('platform-server full application hydration integration', () => {
59405941
);
59415942
});
59425943

5944+
it('should not render content twice with contentChildren', async () => {
5945+
// (globalThis as any).ngDevMode = false;
5946+
@Component({
5947+
selector: 'app-shell',
5948+
imports: [NgTemplateOutlet],
5949+
template: `
5950+
<ng-container [ngTemplateOutlet]="customTemplate"></ng-container>
5951+
`,
5952+
})
5953+
class ShellCmp {
5954+
@ContentChild('customTemplate', {static: true})
5955+
customTemplate: TemplateRef<any> | null = null;
5956+
}
5957+
5958+
@Component({
5959+
imports: [ShellCmp],
5960+
selector: 'app',
5961+
template: `
5962+
<app-shell>
5963+
<ng-template #customTemplate>
5964+
<p>template</p>
5965+
</ng-template>
5966+
</app-shell>
5967+
`,
5968+
})
5969+
class SimpleComponent {}
5970+
5971+
const html = await ssr(SimpleComponent);
5972+
const ssrContents = getAppContents(html);
5973+
expect(ssrContents).toContain('<app ngh');
5974+
5975+
resetTViewsFor(SimpleComponent, ShellCmp);
5976+
5977+
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent);
5978+
const compRef = getComponentRef<SimpleComponent>(appRef);
5979+
appRef.tick();
5980+
5981+
const clientRootNode = compRef.location.nativeElement;
5982+
verifyAllNodesClaimedForHydration(clientRootNode);
5983+
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
5984+
}, 100_000);
5985+
59435986
it('should handle projected containers inside other containers', async () => {
59445987
@Component({
59455988
standalone: true,

0 commit comments

Comments
 (0)
0