Skip to content

Commit de85979

Browse files
committed
fix(core): skip hydration for i18n nodes that were not projected (#57356)
This commit fixes an issue that happens when an i18n block is defined as a projectable content, but a parent component doesn't project it. With an extra check added in this commit, the code will be taking a regular "creation" pass instead of attempting hydration. Resolves #57301. PR Close #57356
1 parent bbc970b commit de85979

File tree

3 files changed

+63
-7
lines changed

3 files changed

+63
-7
lines changed

packages/core/src/hydration/i18n.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {IS_I18N_HYDRATION_ENABLED} from './tokens';
2626
import {
2727
getNgContainerSize,
2828
initDisconnectedNodes,
29+
isDisconnectedNode,
2930
isSerializedElementContainer,
3031
processTextNodeBeforeSerialization,
3132
} from './utils';
@@ -407,15 +408,17 @@ function prepareI18nBlockForHydrationImpl(
407408
parentTNode: TNode | null,
408409
subTemplateIndex: number,
409410
) {
410-
if (
411-
!isI18nHydrationSupportEnabled() ||
412-
(parentTNode && isI18nInSkipHydrationBlock(parentTNode))
413-
) {
411+
const hydrationInfo = lView[HYDRATION];
412+
if (!hydrationInfo) {
414413
return;
415414
}
416415

417-
const hydrationInfo = lView[HYDRATION];
418-
if (!hydrationInfo) {
416+
if (
417+
!isI18nHydrationSupportEnabled() ||
418+
(parentTNode &&
419+
(isI18nInSkipHydrationBlock(parentTNode) ||
420+
isDisconnectedNode(hydrationInfo, parentTNode.index - HEADER_OFFSET)))
421+
) {
419422
return;
420423
}
421424

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {validateMatchingNode, validateNodeExists} from '../../hydration/error_ha
99
import {locateNextRNode, siblingAfter} from '../../hydration/node_lookup_utils';
1010
import {
1111
getNgContainerSize,
12+
isDisconnectedNode,
1213
markRNodeAsClaimedByHydration,
1314
setSegmentHead,
1415
} from '../../hydration/utils';
@@ -204,7 +205,11 @@ function locateOrCreateElementContainerNode(
204205
): RComment {
205206
let comment: RComment;
206207
const hydrationInfo = lView[HYDRATION];
207-
const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock() || isDetachedByI18n(tNode);
208+
const isNodeCreationMode =
209+
!hydrationInfo ||
210+
isInSkipHydrationBlock() ||
211+
isDisconnectedNode(hydrationInfo, index) ||
212+
isDetachedByI18n(tNode);
208213

209214
lastNodeWasCreated(isNodeCreationMode);
210215

packages/platform-server/test/hydration_spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,54 @@ describe('platform-server hydration integration', () => {
21282128
expect(content.innerHTML).toBe('<span>two</span><div>one</div>');
21292129
});
21302130

2131+
it('should work when i18n content is not projected', async () => {
2132+
@Component({
2133+
standalone: true,
2134+
selector: 'app-content',
2135+
template: `
2136+
@if (false) {
2137+
<ng-content />
2138+
}
2139+
Content outside of 'if'.
2140+
`,
2141+
})
2142+
class ContentComponent {}
2143+
2144+
@Component({
2145+
standalone: true,
2146+
selector: 'app',
2147+
template: `
2148+
<app-content>
2149+
<div i18n>Hello!</div>
2150+
<ng-container i18n>Hello again!</ng-container>
2151+
</app-content>
2152+
`,
2153+
imports: [ContentComponent],
2154+
})
2155+
class SimpleComponent {}
2156+
2157+
const hydrationFeatures = [withI18nSupport()] as unknown as HydrationFeature<any>[];
2158+
const html = await ssr(SimpleComponent, {hydrationFeatures});
2159+
2160+
const ssrContents = getAppContents(html);
2161+
expect(ssrContents).toContain('<app ngh');
2162+
2163+
resetTViewsFor(SimpleComponent, ContentComponent);
2164+
2165+
const appRef = await renderAndHydrate(doc, html, SimpleComponent, {hydrationFeatures});
2166+
const compRef = getComponentRef<SimpleComponent>(appRef);
2167+
appRef.tick();
2168+
2169+
const clientRootNode = compRef.location.nativeElement;
2170+
verifyAllNodesClaimedForHydration(clientRootNode);
2171+
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
2172+
2173+
const content = clientRootNode.querySelector('app-content');
2174+
const text = content.textContent.trim();
2175+
expect(text).toBe("Content outside of 'if'.");
2176+
expect(text).not.toContain('Hello');
2177+
});
2178+
21312179
it('should support interleaving projected content', async () => {
21322180
@Component({
21332181
standalone: true,

0 commit comments

Comments
 (0)