Skip to content

Commit 286012f

Browse files
AndrewKushnirdylhunn
authored andcommitted
fix(core): handle hydration of components that project content conditionally (#57383)
This commit fixes an issue when hydration serialization tries to calculate DOM path to a content projection node (`<ng-content>`), but such nodes do not have DOM representation. Resolves #56750. PR Close #57383
1 parent 9155f75 commit 286012f

File tree

2 files changed

+72
-0
lines changed

2 files changed

+72
-0
lines changed

packages/core/src/hydration/annotate.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,12 @@ function conditionallyAnnotateNodePath(
583583
lView: LView<unknown>,
584584
excludedParentNodes: Set<number> | null,
585585
) {
586+
if (isProjectionTNode(tNode)) {
587+
// Do not annotate projection nodes (<ng-content />), since
588+
// they don't have a corresponding DOM node representing them.
589+
return;
590+
}
591+
586592
// Handle case #1 described above.
587593
if (
588594
tNode.projectionNext &&

packages/platform-server/test/hydration_spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5685,6 +5685,72 @@ describe('platform-server hydration integration', () => {
56855685
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
56865686
});
56875687

5688+
it('should support cases when some element nodes are not projected', async () => {
5689+
@Component({
5690+
standalone: true,
5691+
selector: 'app-dropdown-content',
5692+
template: `<ng-content />`,
5693+
})
5694+
class DropdownContentComponent {}
5695+
5696+
@Component({
5697+
standalone: true,
5698+
selector: 'app-dropdown',
5699+
template: `
5700+
@if (false) {
5701+
<ng-content select="app-dropdown-content" />
5702+
}
5703+
`,
5704+
})
5705+
class DropdownComponent {}
5706+
5707+
@Component({
5708+
standalone: true,
5709+
imports: [DropdownComponent, DropdownContentComponent],
5710+
selector: 'app-menu',
5711+
template: `
5712+
<app-dropdown>
5713+
<app-dropdown-content>
5714+
<ng-content />
5715+
</app-dropdown-content>
5716+
</app-dropdown>
5717+
`,
5718+
})
5719+
class MenuComponent {}
5720+
5721+
@Component({
5722+
selector: 'app',
5723+
standalone: true,
5724+
imports: [MenuComponent],
5725+
template: `
5726+
<app-menu>
5727+
Menu Content
5728+
</app-menu>
5729+
`,
5730+
})
5731+
class SimpleComponent {}
5732+
5733+
const html = await ssr(SimpleComponent);
5734+
const ssrContents = getAppContents(html);
5735+
5736+
expect(ssrContents).toContain('<app ngh');
5737+
5738+
resetTViewsFor(
5739+
SimpleComponent,
5740+
MenuComponent,
5741+
DropdownComponent,
5742+
DropdownContentComponent,
5743+
);
5744+
5745+
const appRef = await renderAndHydrate(doc, html, SimpleComponent);
5746+
const compRef = getComponentRef<SimpleComponent>(appRef);
5747+
appRef.tick();
5748+
5749+
const clientRootNode = compRef.location.nativeElement;
5750+
verifyAllNodesClaimedForHydration(clientRootNode);
5751+
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
5752+
});
5753+
56885754
it('should support cases when view containers are not projected', async () => {
56895755
@Component({
56905756
standalone: true,

0 commit comments

Comments
 (0)