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

Skip to content

Commit 8424b3b

Browse files
fix(core): Fixes template outlet hydration (#61989)
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 #61989
1 parent 61fb6e7 commit 8424b3b

File tree

3 files changed

+56
-15
lines changed

3 files changed

+56
-15
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: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,7 @@ function locateOrCreateContainerAnchorImpl(
351351
const isNodeCreationMode = !canHydrateNode(lView, tNode);
352352
lastNodeWasCreated(isNodeCreationMode);
353353

354-
// Regular creation mode.
355-
if (isNodeCreationMode) {
356-
return createContainerAnchorImpl(tView, lView, tNode, index);
357-
}
358-
359-
const hydrationInfo = lView[HYDRATION]!;
360-
const ssrId = hydrationInfo.data[TEMPLATES]?.[index] ?? null;
354+
const ssrId = lView[HYDRATION]?.data[TEMPLATES]?.[index] ?? null;
361355

362356
// Apply `ssrId` value to the underlying TView if it was not previously set.
363357
//
@@ -376,6 +370,12 @@ function locateOrCreateContainerAnchorImpl(
376370
}
377371
}
378372

373+
// Regular creation mode.
374+
if (isNodeCreationMode) {
375+
return createContainerAnchorImpl(tView, lView, tNode, index);
376+
}
377+
378+
const hydrationInfo = lView[HYDRATION]!;
379379
// Hydration mode, looking up existing elements in DOM.
380380
const currentRNode = locateNextRNode(hydrationInfo, tView, lView, tNode)!;
381381
ngDevMode && validateNodeExists(currentRNode, lView, tNode);

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