10000 [PATCH] fix(core): Fixes template outlet hydration by thePunderWoman · Pull Request #62012 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

[PATCH] fix(core): Fixes template outlet hydration #62012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions packages/core/src/hydration/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,10 +595,15 @@ function serializeLView(
continue;
}

// Serialize information about template.
if (isLContainer(lView[i]) && tNode.tView) {
ngh[TEMPLATES] ??= {};
ngh[TEMPLATES][noOffsetIndex] = getSsrId(tNode.tView!);
}

// 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).
Expand Down Expand Up @@ -648,13 +653,6 @@ function serializeLView(

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

Expand Down
19 changes: 10 additions & 9 deletions packages/core/src/render3/instructions/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,19 +264,15 @@ function locateOrCreateContainerAnchorImpl(
index: number,
): RComment {
const hydrationInfo = lView[HYDRATION];

const isNodeCreationMode =
!hydrationInfo ||
isInSkipHydrationBlock() ||
isDetachedByI18n(tNode) ||
isDisconnectedNode(hydrationInfo, index);
lastNodeWasCreated(isNodeCreationMode);

// Regular creation mode.
if (isNodeCreationMode) {
return createContainerAnchorImpl(tView, lView, tNode, index);
}

const ssrId = hydrationInfo.data[TEMPLATES]?.[index] ?? null;
const ssrId = hydrationInfo?.data[TEMPLATES]?.[index] ?? null;

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

// Regular creation mode.
if (isNodeCreationMode) {
return createContainerAnchorImpl(tView, lView, tNode, index);
}

// Hydration mode, looking up existing elements in DOM.
const currentRNode = locateNextRNode(hydrationInfo, tView, lView, tNode)!;
const currentRNode = locateNextRNode(hydrationInfo!, tView, lView, tNode)!;
ngDevMode && validateNodeExists(currentRNode, lView, tNode);

setSegmentHead(hydrationInfo, index, currentRNode);
const viewContainerSize = calcSerializedContainerSize(hydrationInfo, index);
setSegmentHead(hydrationInfo!, index, currentRNode);
const viewContainerSize = calcSerializedContainerSize(hydrationInfo!, index);
const comment = siblingAfter<RComment>(viewContainerSize, currentRNode)!;

if (ngDevMode) {
Expand Down
43 changes: 43 additions & 0 deletions packages/platform-server/test/full_app_hydration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
ViewContainerRef,
ViewEncapsulation,
ɵNoopNgZone as NoopNgZone,
ContentChild,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {clearTranslations, loadTranslations} from '@angular/localize';
Expand Down Expand Up @@ -5940,6 +5941,48 @@ describe('platform-server full application hydration integration', () => {
);
});

it('should not render content twice with contentChildren', async () => {
// (globalThis as any).ngDevMode = false;
@Component({
selector: 'app-shell',
imports: [NgTemplateOutlet],
template: `
<ng-container [ngTemplateOutlet]="customTemplate"></ng-container>
`,
})
class ShellCmp {
@ContentChild('customTemplate', {static: true})
customTemplate: TemplateRef<any> | null = null;
}

@Component({
imports: [ShellCmp],
selector: 'app',
template: `
<app-shell>
<ng-template #customTemplate>
<p>template</p>
</ng-template>
</app-shell>
`,
})
class SimpleComponent {}

const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngh');

resetTViewsFor(SimpleComponent, ShellCmp);

const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();

const clientRootNode = compRef.location.nativeElement;
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
}, 100_000);

it('should handle projected containers inside other containers', async () => {
@Component({
standalone: true,
Expand Down
Loading
0