From d0a736e46461cfe9dbd3b35c4d0b3b1303776d9e Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Tue, 6 Aug 2024 19:02:49 -0700 Subject: [PATCH] fix(core): take skip hydration flag into account while hydrating i18n blocks This commit updates serialization and hydration i18n logic to take into account situations when i18n blocks are located within "skip hydration" blocks. Resolves #57105. --- packages/core/src/hydration/annotate.ts | 1 - packages/core/src/hydration/i18n.ts | 11 ++- packages/core/src/hydration/skip_hydration.ts | 13 +++ .../core/src/linker/view_container_ref.ts | 2 +- packages/core/src/render3/i18n/i18n_parse.ts | 1 + packages/core/src/render3/interfaces/i18n.ts | 5 + packages/core/test/render3/i18n/i18n_spec.ts | 9 ++ packages/core/test/render3/is_shape_of.ts | 1 + .../platform-server/test/hydration_spec.ts | 95 +++++++++++++++++++ 9 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/core/src/hydration/annotate.ts b/packages/core/src/hydration/annotate.ts index 9859556a20779..75258ca04d22d 100644 --- a/packages/core/src/hydration/annotate.ts +++ b/packages/core/src/hydration/annotate.ts @@ -7,7 +7,6 @@ */ import {ApplicationRef} from '../application/application_ref'; -import {APP_ID} from '../application/application_tokens'; import {isDetachedByI18n} from '../i18n/utils'; import {ViewEncapsulation} from '../metadata'; import {Renderer2} from '../render'; diff --git a/packages/core/src/hydration/i18n.ts b/packages/core/src/hydration/i18n.ts index efdda0cc16a62..68e66e5216fa8 100644 --- a/packages/core/src/hydration/i18n.ts +++ b/packages/core/src/hydration/i18n.ts @@ -21,6 +21,7 @@ import {assertDefined, assertNotEqual} from '../util/assert'; import type {HydrationContext} from './annotate'; import {DehydratedIcuData, DehydratedView, I18N_DATA} from './interfaces'; import {isDisconnectedRNode, locateNextRNode, tryLocateRNodeByPath} from './node_lookup_utils'; +import {isI18nInSkipHydrationBlock} from './skip_hydration'; import {IS_I18N_HYDRATION_ENABLED} from './tokens'; import { getNgContainerSize, @@ -187,6 +188,11 @@ export function trySerializeI18nBlock( return null; } + const parentTNode = tView.data[tI18n.parentTNodeIndex] as TNode; + if (parentTNode && isI18nInSkipHydrationBlock(parentTNode)) { + return null; + } + const serializedI18nBlock: SerializedI18nBlock = { caseQueue: [], disconnectedNodes: new Set(), @@ -401,7 +407,10 @@ function prepareI18nBlockForHydrationImpl( parentTNode: TNode | null, subTemplateIndex: number, ) { - if (!isI18nHydrationSupportEnabled()) { + if ( + !isI18nHydrationSupportEnabled() || + (parentTNode && isI18nInSkipHydrationBlock(parentTNode)) + ) { return; } diff --git a/packages/core/src/hydration/skip_hydration.ts b/packages/core/src/hydration/skip_hydration.ts index 184dfdb6544ff..6ca8e17f9632a 100644 --- a/packages/core/src/hydration/skip_hydration.ts +++ b/packages/core/src/hydration/skip_hydration.ts @@ -70,3 +70,16 @@ export function isInSkipHydrationBlock(tNode: TNode): boolean { } return false; } + +/** + * Check if an i18n block is in a skip hydration section by looking at a parent TNode + * to determine if this TNode is in a skip hydration section or the TNode has + * the `ngSkipHydration` attribute. + */ +export function isI18nInSkipHydrationBlock(parentTNode: TNode): boolean { + return ( + hasInSkipHydrationBlockFlag(parentTNode) || + hasSkipHydrationAttrOnTNode(parentTNode) || + isInSkipHydrationBlock(parentTNode) + ); +} diff --git a/packages/core/src/linker/view_container_ref.ts b/packages/core/src/linker/view_container_ref.ts index e619e71834d15..2614cb92e5185 100644 --- a/packages/core/src/linker/view_container_ref.ts +++ b/packages/core/src/linker/view_container_ref.ts @@ -10,7 +10,7 @@ import {Injector} from '../di/injector'; import {EnvironmentInjector} from '../di/r3_injector'; import {validateMatchingNode} from '../hydration/error_handling'; import {CONTAINERS} from '../hydration/interfaces'; -import {hasInSkipHydrationBlockFlag, isInSkipHydrationBlock} from '../hydration/skip_hydration'; +import {isInSkipHydrationBlock} from '../hydration/skip_hydration'; import { getSegmentHead, isDisconnectedNode, diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts index c1cf4592d86f5..585876d0fa5be 100644 --- a/packages/core/src/render3/i18n/i18n_parse.ts +++ b/packages/core/src/render3/i18n/i18n_parse.ts @@ -236,6 +236,7 @@ export function i18nStartFirstCreatePass( create: createOpCodes, update: updateOpCodes, ast: astStack[0], + parentTNodeIndex, }; } diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts index 2802e40e87fcd..a7350cfca2d69 100644 --- a/packages/core/src/render3/interfaces/i18n.ts +++ b/packages/core/src/render3/interfaces/i18n.ts @@ -345,6 +345,11 @@ export interface TI18n { * while the Update and Create OpCodes are used at runtime. */ ast: Array; + + /** + * Index of a parent TNode, which represents a host node for this i18n block. + */ + parentTNodeIndex: number; } /** diff --git a/packages/core/test/render3/i18n/i18n_spec.ts b/packages/core/test/render3/i18n/i18n_spec.ts index edf05f0efdc80..76aa51f7abe4b 100644 --- a/packages/core/test/render3/i18n/i18n_spec.ts +++ b/packages/core/test/render3/i18n/i18n_spec.ts @@ -105,6 +105,7 @@ describe('Runtime i18n', () => { ]), update: [] as unknown as I18nUpdateOpCodes, ast: [{kind: 0, index: HEADER_OFFSET + 1}], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -154,6 +155,7 @@ describe('Runtime i18n', () => { }, {kind: 0, index: HEADER_OFFSET + 8}, ], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -189,6 +191,7 @@ describe('Runtime i18n', () => { }] as Text).textContent = \`Hello \${lView[i-1]}!\`; }`, ]), ast: [{kind: 0, index: HEADER_OFFSET + 2}], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -218,6 +221,7 @@ describe('Runtime i18n', () => { }] as Text).textContent = \`Hello \${lView[i-1]} and \${lView[i-2]}, again \${lView[i-1]}!\`; }`, ]), ast: [{kind: 0, index: HEADER_OFFSET + 2}], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -264,6 +268,7 @@ describe('Runtime i18n', () => { {kind: 2, index: HEADER_OFFSET + 2, children: [], type: 1}, {kind: 0, index: HEADER_OFFSET + 4}, ], + parentTNodeIndex: HEADER_OFFSET, }); /**** First sub-template ****/ @@ -298,6 +303,7 @@ describe('Runtime i18n', () => { type: 0, }, ], + parentTNodeIndex: HEADER_OFFSET, }); /**** Second sub-template ****/ @@ -325,6 +331,7 @@ describe('Runtime i18n', () => { type: 0, }, ], + parentTNodeIndex: HEADER_OFFSET, }); }); @@ -390,6 +397,7 @@ describe('Runtime i18n', () => { currentCaseLViewIndex: HEADER_OFFSET + 3, }, ], + parentTNodeIndex: HEADER_OFFSET, }); expect(getTIcu(tView, HEADER_OFFSET + 2)).toEqual({ type: 1, @@ -511,6 +519,7 @@ describe('Runtime i18n', () => { currentCaseLViewIndex: HEADER_OFFSET + 3, }, ], + parentTNodeIndex: HEADER_OFFSET, }); expect(getTIcu(tView, HEADER_OFFSET + 2)).toEqual({ type: 1, diff --git a/packages/core/test/render3/is_shape_of.ts b/packages/core/test/render3/is_shape_of.ts index b7ce9d5f9c150..6a2db4472525b 100644 --- a/packages/core/test/render3/is_shape_of.ts +++ b/packages/core/test/render3/is_shape_of.ts @@ -77,6 +77,7 @@ const ShapeOfTI18n: ShapeOf = { create: true, update: true, ast: true, + parentTNodeIndex: true, }; /** diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index 3e609436043bf..7b5f9fc345174 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -117,6 +117,10 @@ function verifyNodeHasMismatchInfo(doc: Document, selector = 'app'): void { expect(readHydrationInfo(doc.querySelector(selector)!)?.status).toBe(HydrationStatus.Mismatched); } +function verifyNodeHasSkipHydrationMarker(element: HTMLElement): void { + expect(readHydrationInfo(element)?.status).toBe(HydrationStatus.Skipped); +} + /** Checks whether a given element is a