diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 5b56e184c81..ddc79340486 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -25,7 +25,7 @@ export {INJECTOR_SCOPE as ɵINJECTOR_SCOPE} from './di/scope'; export {XSS_SECURITY_URL as ɵXSS_SECURITY_URL} from './error_details_base_url'; export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeError, RuntimeErrorCode as ɵRuntimeErrorCode} from './errors'; export {annotateForHydration as ɵannotateForHydration} from './hydration/annotate'; -export {withDomHydration as ɵwithDomHydration} from './hydration/api'; +export {withDomHydration as ɵwithDomHydration, withI18nHydration as ɵwithI18nHydration} from './hydration/api'; export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from './hydration/tokens'; export {HydratedNode as ɵHydratedNode, HydrationInfo as ɵHydrationInfo, readHydrationInfo as ɵreadHydrationInfo, SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER} from './hydration/utils'; export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api'; diff --git a/packages/core/src/hydration/annotate.ts b/packages/core/src/hydration/annotate.ts index 17c2e11a90c..3c2ea73631c 100644 --- a/packages/core/src/hydration/annotate.ts +++ b/packages/core/src/hydration/annotate.ts @@ -19,6 +19,7 @@ import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TView, TVIEW, TVi import {unwrapLView, unwrapRNode} from '../render3/util/view_utils'; import {TransferState} from '../transfer_state'; +import {isI18nHydrationSupportEnabled} from './api'; import {unsupportedProjectionOfDomNodes} from './error_handling'; import {CONTAINERS, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NODES, NUM_ROOT_NODES, SerializedContainerView, SerializedView, TEMPLATE_ID, TEMPLATES} from './interfaces'; import {calcPathForNode, isDisconnectedNode} from './node_lookup_utils'; @@ -526,7 +527,8 @@ function componentUsesShadowDomEncapsulation(lView: LView): boolean { function annotateHostElementForHydration( element: RElement, lView: LView, context: HydrationContext): number|null { const renderer = lView[RENDERER]; - if (hasI18n(lView) || componentUsesShadowDomEncapsulation(lView)) { + if ((hasI18n(lView) && !isI18nHydrationSupportEnabled()) || + componentUsesShadowDomEncapsulation(lView)) { // Attach the skip hydration attribute if this component: // - either has i18n blocks, since hydrating such blocks is not yet supported // - or uses ShadowDom view encapsulation, since Domino doesn't support diff --git a/packages/core/src/hydration/api.ts b/packages/core/src/hydration/api.ts index 3702cf857b5..1b842137323 100644 --- a/packages/core/src/hydration/api.ts +++ b/packages/core/src/hydration/api.ts @@ -12,6 +12,7 @@ import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, Injector, makeEnvironment import {inject} from '../di/injector_compatibility'; import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors'; import {enableLocateOrCreateContainerRefImpl} from '../linker/view_container_ref'; +import {enableLocateOrCreateI18nNodeImpl} from '../render3/i18n/i18n_apply'; import {enableLocateOrCreateElementNodeImpl} from '../render3/instructions/element'; import {enableLocateOrCreateElementContainerNodeImpl} from '../render3/instructions/element_container'; import {enableApplyRootElementTransformImpl} from '../render3/instructions/shared'; @@ -24,7 +25,7 @@ import {performanceMarkFeature} from '../util/performance'; import {NgZone} from '../zone'; import {cleanupDehydratedViews} from './cleanup'; -import {IS_HYDRATION_DOM_REUSE_ENABLED, PRESERVE_HOST_CONTENT} from './tokens'; +import {IS_HYDRATION_DOM_REUSE_ENABLED, IS_I18N_HYDRATION_ENABLED, PRESERVE_HOST_CONTENT} from './tokens'; import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY, SSR_CONTENT_INTEGRITY_MARKER} from './utils'; import {enableFindMatchingDehydratedViewImpl} from './views'; @@ -34,6 +35,11 @@ import {enableFindMatchingDehydratedViewImpl} from './views'; */ let isHydrationSupportEnabled = false; +/** + * Indicates whether support for hydrating i18n blocks is enabled. + */ +let _isI18nHydrationSupportEnabled = false; + /** * Defines a period of time that Angular waits for the `ApplicationRef.isStable` to emit `true`. * If there was no event with the `true` value during this time, Angular reports a warning. @@ -62,6 +68,7 @@ function enableHydrationRuntimeSupport() { enableLocateOrCreateContainerRefImpl(); enableFindMatchingDehydratedViewImpl(); enableApplyRootElementTransformImpl(); + enableLocateOrCreateI18nNodeImpl(); } } @@ -145,6 +152,8 @@ export function withDomHydration(): EnvironmentProviders { { provide: ENVIRONMENT_INITIALIZER, useValue: () => { + _isI18nHydrationSupportEnabled = !!inject(IS_I18N_HYDRATION_ENABLED, {optional: true}); + // Since this function is used across both server and client, // make sure that the runtime code is only added when invoked // on the client. Moving forward, the `isPlatformBrowser` check should @@ -198,6 +207,26 @@ export function withDomHydration(): EnvironmentProviders { ]); } +/** + * Returns a set of providers required to setup support for i18n hydration. + * Requires hydration to be enabled separately. + */ +export function withI18nHydration(): EnvironmentProviders { + return makeEnvironmentProviders([ + { + provide: IS_I18N_HYDRATION_ENABLED, + useValue: true, + }, + ]); +} + +/** + * Returns whether i18n hydration support is enabled. + */ +export function isI18nHydrationSupportEnabled(): boolean { + return _isI18nHydrationSupportEnabled; +} + /** * * @param time The time in ms until the stable timedout warning message is logged diff --git a/packages/core/src/hydration/tokens.ts b/packages/core/src/hydration/tokens.ts index f732d26b4e3..97bf75df213 100644 --- a/packages/core/src/hydration/tokens.ts +++ b/packages/core/src/hydration/tokens.ts @@ -28,3 +28,10 @@ export const PRESERVE_HOST_CONTENT = new InjectionToken( providedIn: 'root', factory: () => PRESERVE_HOST_CONTENT_DEFAULT, }); + +/** + * Internal token that indicates whether hydration support for i18n + * is enabled. + */ +export const IS_I18N_HYDRATION_ENABLED = new InjectionToken( + (typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_I18N_HYDRATION_ENABLED' : '')); diff --git a/packages/core/src/render3/i18n/i18n_apply.ts b/packages/core/src/render3/i18n/i18n_apply.ts index 4aacc33904a..70d6d1f2d7e 100644 --- a/packages/core/src/render3/i18n/i18n_apply.ts +++ b/packages/core/src/render3/i18n/i18n_apply.ts @@ -18,7 +18,7 @@ import {RElement, RNode, RText} from '../interfaces/renderer_dom'; import {SanitizerFn} from '../interfaces/sanitization'; import {HEADER_OFFSET, LView, RENDERER, TView} from '../interfaces/view'; import {createCommentNode, createElementNode, createTextNode, nativeInsertBefore, nativeParentNode, nativeRemoveNode, updateTextNode} from '../node_manipulation'; -import {getBindingIndex, lastNodeWasCreated} from '../state'; +import {getBindingIndex, lastNodeWasCreated, wasLastNodeCreated} from '../state'; import {renderStringify} from '../util/stringify_utils'; import {getNativeByIndex, unwrapRNode} from '../util/view_utils'; @@ -78,12 +78,9 @@ export function applyI18n(tView: TView, lView: LView, index: number) { changeMaskCounter = 0; } -function locateOrCreateNode( - lView: LView, index: number, textOrName: string, +function createNodeWithoutHydration( + lView: LView, textOrName: string, nodeType: typeof Node.COMMENT_NODE|typeof Node.TEXT_NODE|typeof Node.ELEMENT_NODE) { - // TODO: Add support for hydration - lastNodeWasCreated(true); - const renderer = lView[RENDERER]; switch (nodeType) { @@ -98,6 +95,23 @@ function locateOrCreateNode( } } +let _locateOrCreateNode: typeof locateOrCreateNodeImpl = (lView, index, textOrName, nodeType) => { + lastNodeWasCreated(true); + return createNodeWithoutHydration(lView, textOrName, nodeType); +}; + +function locateOrCreateNodeImpl( + lView: LView, index: number, textOrName: string, + nodeType: typeof Node.COMMENT_NODE|typeof Node.TEXT_NODE|typeof Node.ELEMENT_NODE) { + // TODO: Add support for hydration + lastNodeWasCreated(true); + return createNodeWithoutHydration(lView, textOrName, nodeType); +} + +export function enableLocateOrCreateI18nNodeImpl() { + _locateOrCreateNode = locateOrCreateNodeImpl; +} + /** * Apply `I18nCreateOpCodes` op-codes as stored in `TI18n.create`. * @@ -121,13 +135,15 @@ export function applyCreateOpCodes( (opCode & I18nCreateOpCode.APPEND_EAGERLY) === I18nCreateOpCode.APPEND_EAGERLY; const index = opCode >>> I18nCreateOpCode.SHIFT; let rNode = lView[index]; + let lastNodeWasCreated = false; if (rNode === null) { // We only create new DOM nodes if they don't already exist: If ICU switches case back to a // case which was already instantiated, no need to create new DOM nodes. rNode = lView[index] = - locateOrCreateNode(lView, index, text, isComment ? Node.COMMENT_NODE : Node.TEXT_NODE); + _locateOrCreateNode(lView, index, text, isComment ? Node.COMMENT_NODE : Node.TEXT_NODE); + lastNodeWasCreated = wasLastNodeCreated(); } - if (appendNow && parentRNode !== null) { + if (appendNow && parentRNode !== null && lastNodeWasCreated) { nativeInsertBefore(renderer, parentRNode, rNode, insertInFrontOf, false); } } @@ -160,7 +176,7 @@ export function applyMutableOpCodes( if (lView[textNodeIndex] === null) { ngDevMode && ngDevMode.rendererCreateTextNode++; ngDevMode && assertIndexInRange(lView, textNodeIndex); - lView[textNodeIndex] = locateOrCreateNode(lView, textNodeIndex, opCode, Node.TEXT_NODE); + lView[textNodeIndex] = _locateOrCreateNode(lView, textNodeIndex, opCode, Node.TEXT_NODE); } } else if (typeof opCode == 'number') { switch (opCode & IcuCreateOpCode.MASK_INSTRUCTION) { @@ -238,7 +254,7 @@ export function applyMutableOpCodes( ngDevMode && ngDevMode.rendererCreateComment++; ngDevMode && assertIndexInExpandoRange(lView, commentNodeIndex); const commentRNode = lView[commentNodeIndex] = - locateOrCreateNode(lView, commentNodeIndex, commentValue, Node.COMMENT_NODE); + _locateOrCreateNode(lView, commentNodeIndex, commentValue, Node.COMMENT_NODE); // FIXME(misko): Attaching patch data is only needed for the root (Also add tests) attachPatchData(commentRNode, lView); } @@ -255,7 +271,7 @@ export function applyMutableOpCodes( ngDevMode && ngDevMode.rendererCreateElement++; ngDevMode && assertIndexInExpandoRange(lView, elementNodeIndex); const elementRNode = lView[elementNodeIndex] = - locateOrCreateNode(lView, elementNodeIndex, tagName, Node.ELEMENT_NODE); + _locateOrCreateNode(lView, elementNodeIndex, tagName, Node.ELEMENT_NODE); // FIXME(misko): Attaching patch data is only needed for the root (Also add tests) attachPatchData(elementRNode, lView); } diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index 89d39a7db6e..dfcdb0e5dfc 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -215,6 +215,9 @@ { "name": "IS_HYDRATION_DOM_REUSE_ENABLED" }, + { + "name": "IS_I18N_HYDRATION_ENABLED" + }, { "name": "InjectFlags" }, diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index df2a700d839..7e2450e47b6 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -10,7 +10,7 @@ import '@angular/localize/init'; import {CommonModule, DOCUMENT, isPlatformServer, NgComponentOutlet, NgFor, NgIf, NgTemplateOutlet, PlatformLocation} from '@angular/common'; import {MockPlatformLocation} from '@angular/common/testing'; -import {afterRender, ApplicationRef, Component, ComponentRef, ContentChildren, createComponent, destroyPlatform, Directive, ElementRef, EnvironmentInjector, ErrorHandler, getPlatform, inject, Injectable, Input, NgZone, PLATFORM_ID, Provider, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument, ɵwhenStable as whenStable} from '@angular/core'; +import {afterRender, ApplicationRef, Component, ComponentRef, ContentChildren, createComponent, destroyPlatform, Directive, ElementRef, EnvironmentInjector, ErrorHandler, getPlatform, inject, Injectable, Input, NgZone, PLATFORM_ID, Provider, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument, ɵwhenStable as whenStable, ɵwithI18nHydration as withI18nHydration} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {HydrationStatus, readHydrationInfo, SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils'; import {getComponentDef} from '@angular/core/src/render3/definition'; @@ -1996,254 +1996,333 @@ describe('platform-server hydration integration', () => { }); }); - // Note: hydration for i18n blocks is not *yet* supported, so the tests - // below verify that components that use i18n are excluded from the hydration - // by adding the `ngSkipHydration` flag onto the component host element. describe('i18n', () => { - it('should append skip hydration flag if component uses i18n blocks', async () => { - @Component({ - standalone: true, - selector: 'app', - template: ` + describe('support is enabled', () => { + it('should not append skip hydration flag if component uses i18n blocks', async () => { + @Component({ + standalone: true, + selector: 'app', + template: `
Hi!
`, - }) - class SimpleComponent { - } + }) + class SimpleComponent { + } - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); - expect(ssrContents).toContain(''); + const providers = [withI18nHydration()] as unknown as Provider[]; + const html = await ssr(SimpleComponent, undefined, providers); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain('(appRef); - appRef.tick(); - - const clientRootNode = compRef.location.nativeElement; - verifyNoNodesWereClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); - }); - - it('should keep the skip hydration flag if component uses i18n blocks', async () => { - @Component({ - standalone: true, - selector: 'app', - host: {ngSkipHydration: 'true'}, - template: ` -
Hi!
- `, - }) - class SimpleComponent { - } - - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); - expect(ssrContents).toContain(''); - - resetTViewsFor(SimpleComponent); - - const appRef = await hydrate(html, SimpleComponent); - const compRef = getComponentRef(appRef); - appRef.tick(); - - const clientRootNode = compRef.location.nativeElement; - verifyNoNodesWereClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); - }); - - it('should append skip hydration flag if component uses i18n blocks inside embedded views', - async () => { - @Component({ - standalone: true, - imports: [NgIf], - selector: 'app', - template: ` + it('should not append skip hydration flag if component uses i18n blocks inside embedded views', + async () => { + @Component({ + standalone: true, + imports: [NgIf], + selector: 'app', + template: `
Hi!
`, - }) - class SimpleComponent { - } + }) + class SimpleComponent { + } - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); - expect(ssrContents).toContain(''); + const providers = [withI18nHydration()] as unknown as Provider[]; + const html = await ssr(SimpleComponent, undefined, providers); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain('(appRef); - appRef.tick(); - - const clientRootNode = compRef.location.nativeElement; - verifyNoNodesWereClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); - }); - - it('should append skip hydration flag if component uses i18n blocks on s', - async () => { - @Component({ - standalone: true, - selector: 'app', - template: ` + it('should not append skip hydration flag if component uses i18n blocks on s', + async () => { + @Component({ + standalone: true, + selector: 'app', + template: ` Hi! `, - }) - class SimpleComponent { - } + }) + class SimpleComponent { + } - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); - expect(ssrContents).toContain(''); + const providers = [withI18nHydration()] as unknown as Provider[]; + const html = await ssr(SimpleComponent, undefined, providers); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain('(appRef); - appRef.tick(); - - const clientRootNode = compRef.location.nativeElement; - verifyNoNodesWereClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); - }); - - it('should append skip hydration flag if component uses i18n blocks (with *ngIfs on s)', - async () => { - @Component({ - standalone: true, - imports: [CommonModule], - selector: 'app', - template: ` + it('should not append skip hydration flag if component uses i18n blocks (with *ngIfs on s)', + async () => { + @Component({ + standalone: true, + imports: [CommonModule], + selector: 'app', + template: ` Hi! `, - }) - class SimpleComponent { - } + }) + class SimpleComponent { + } - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); - expect(ssrContents).toContain(''); - - resetTViewsFor(SimpleComponent); - - const appRef = await hydrate(html, SimpleComponent); - const compRef = getComponentRef(appRef); - appRef.tick(); - - const clientRootNode = compRef.location.nativeElement; - verifyNoNodesWereClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); - }); - - it('should *not* throw when i18n attributes are used', async () => { - @Component({ - standalone: true, - selector: 'app', - template: ` -
Hi!
- `, - }) - class SimpleComponent { - } - - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); - - expect(ssrContents).toContain('(appRef); - appRef.tick(); - - const clientRootNode = compRef.location.nativeElement; - verifyAllNodesClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + const providers = [withI18nHydration()] as unknown as Provider[]; + const html = await ssr(SimpleComponent, undefined, providers); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain(' { - @Component({ - standalone: true, - selector: 'nested', - template: ` + // Note: hydration for i18n blocks is not *yet* fully supported, so the tests + // below verify that components that use i18n are excluded from the hydration + // by adding the `ngSkipHydration` flag onto the component host element. + describe('support is disabled', () => { + it('should append skip hydration flag if component uses i18n blocks', async () => { + @Component({ + standalone: true, + selector: 'app', + template: ` +
Hi!
+ `, + }) + class SimpleComponent { + } + + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain(''); + + resetTViewsFor(SimpleComponent); + + const appRef = await hydrate(html, SimpleComponent); + const compRef = getComponentRef(appRef); + appRef.tick(); + + const clientRootNode = compRef.location.nativeElement; + verifyNoNodesWereClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); + + it('should keep the skip hydration flag if component uses i18n blocks', async () => { + @Component({ + standalone: true, + selector: 'app', + host: {ngSkipHydration: 'true'}, + template: ` +
Hi!
+ `, + }) + class SimpleComponent { + } + + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain(''); + + resetTViewsFor(SimpleComponent); + + const appRef = await hydrate(html, SimpleComponent); + const compRef = getComponentRef(appRef); + appRef.tick(); + + const clientRootNode = compRef.location.nativeElement; + verifyNoNodesWereClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); + + it('should append skip hydration flag if component uses i18n blocks inside embedded views', + async () => { + @Component({ + standalone: true, + imports: [NgIf], + selector: 'app', + template: ` +
+
Hi!
+
+ `, + }) + class SimpleComponent { + } + + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain(''); + + resetTViewsFor(SimpleComponent); + + const appRef = await hydrate(html, SimpleComponent); + const compRef = getComponentRef(appRef); + appRef.tick(); + + const clientRootNode = compRef.location.nativeElement; + verifyNoNodesWereClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); + + it('should append skip hydration flag if component uses i18n blocks on s', + async () => { + @Component({ + standalone: true, + selector: 'app', + template: ` + Hi! + `, + }) + class SimpleComponent { + } + + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain(''); + + resetTViewsFor(SimpleComponent); + + const appRef = await hydrate(html, SimpleComponent); + const compRef = getComponentRef(appRef); + appRef.tick(); + + const clientRootNode = compRef.location.nativeElement; + verifyNoNodesWereClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); + + it('should append skip hydration flag if component uses i18n blocks (with *ngIfs on s)', + async () => { + @Component({ + standalone: true, + imports: [CommonModule], + selector: 'app', + template: ` + Hi! + `, + }) + class SimpleComponent { + } + + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); + expect(ssrContents).toContain(''); + + resetTViewsFor(SimpleComponent); + + const appRef = await hydrate(html, SimpleComponent); + const compRef = getComponentRef(appRef); + appRef.tick(); + + const clientRootNode = compRef.location.nativeElement; + verifyNoNodesWereClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); + + it('should *not* throw when i18n attributes are used', async () => { + @Component({ + standalone: true, + selector: 'app', + template: ` +
Hi!
+ `, + }) + class SimpleComponent { + } + + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); + + expect(ssrContents).toContain('(appRef); + appRef.tick(); + + const clientRootNode = compRef.location.nativeElement; + verifyAllNodesClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); + + it('should *not* throw when i18n is used in nested component ' + + 'excluded using `ngSkipHydration`', + async () => { + @Component({ + standalone: true, + selector: 'nested', + template: `
Hi!
`, - }) - class NestedComponent { - } + }) + class NestedComponent { + } - @Component({ - standalone: true, - imports: [NestedComponent], - selector: 'app', - template: ` + @Component({ + standalone: true, + imports: [NestedComponent], + selector: 'app', + template: ` Nested component with i18n inside: `, - }) - class SimpleComponent { - } + }) + class SimpleComponent { + } - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); - expect(ssrContents).toContain('(appRef); - appRef.tick(); + const appRef = await hydrate(html, SimpleComponent); + const compRef = getComponentRef(appRef); + appRef.tick(); - const clientRootNode = compRef.location.nativeElement; - verifyAllNodesClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); - }); + const clientRootNode = compRef.location.nativeElement; + verifyAllNodesClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); - it('should exclude components with i18n from hydration automatically', async () => { - @Component({ - standalone: true, - selector: 'nested', - template: ` + it('should exclude components with i18n from hydration automatically', async () => { + @Component({ + standalone: true, + selector: 'nested', + template: `
Hi!
`, - }) - class NestedComponent { - } + }) + class NestedComponent { + } - @Component({ - standalone: true, - imports: [NestedComponent], - selector: 'app', - template: ` + @Component({ + standalone: true, + imports: [NestedComponent], + selector: 'app', + template: ` Nested component with i18n inside (the content of this component would be excluded from hydration): `, - }) - class SimpleComponent { - } + }) + class SimpleComponent { + } - const html = await ssr(SimpleComponent); - const ssrContents = getAppContents(html); + const html = await ssr(SimpleComponent); + const ssrContents = getAppContents(html); - expect(ssrContents).toContain('(appRef); - appRef.tick(); + const appRef = await hydrate(html, SimpleComponent); + const compRef = getComponentRef(appRef); + appRef.tick(); - const clientRootNode = compRef.location.nativeElement; - verifyAllNodesClaimedForHydration(clientRootNode); - verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + const clientRootNode = compRef.location.nativeElement; + verifyAllNodesClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); + }); }); });