refactor(core): add internal API to enable i18n hydration (#54784)

Add an internal API to enable and use i18n hydration for testing and development. This helps ensure that we don't accidentally break the current behavior until we are completely ready to roll out i18n support.

PR Close #54784
This commit is contained in:
Gerald Monaco 2024-03-08 18:13:15 +00:00 committed by Andrew Scott
parent ac395d0f68
commit cdfcb779df
7 changed files with 355 additions and 219 deletions

View file

@ -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';

View file

@ -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

View file

@ -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

View file

@ -28,3 +28,10 @@ export const PRESERVE_HOST_CONTENT = new InjectionToken<boolean>(
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<boolean>(
(typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_I18N_HYDRATION_ENABLED' : ''));

View file

@ -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);
}

View file

@ -215,6 +215,9 @@
{
"name": "IS_HYDRATION_DOM_REUSE_ENABLED"
},
{
"name": "IS_I18N_HYDRATION_ENABLED"
},
{
"name": "InjectFlags"
},

View file

@ -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: `
<div i18n>Hi!</div>
`,
})
class SimpleComponent {
}
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
const providers = [withI18nHydration()] as unknown as Provider[];
const html = await ssr(SimpleComponent, undefined, providers);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngh');
});
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<div i18n>Hi!</div>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="true">');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<main *ngIf="true">
<div *ngIf="true" i18n>Hi!</div>
</main>
`,
})
class SimpleComponent {
}
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
const providers = [withI18nHydration()] as unknown as Provider[];
const html = await ssr(SimpleComponent, undefined, providers);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngh');
});
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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 <ng-container>s',
async () => {
@Component({
standalone: true,
selector: 'app',
template: `
it('should not append skip hydration flag if component uses i18n blocks on <ng-container>s',
async () => {
@Component({
standalone: true,
selector: 'app',
template: `
<ng-container i18n>Hi!</ng-container>
`,
})
class SimpleComponent {
}
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
const providers = [withI18nHydration()] as unknown as Provider[];
const html = await ssr(SimpleComponent, undefined, providers);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngh');
});
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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 <ng-container>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 <ng-container>s)',
async () => {
@Component({
standalone: true,
imports: [CommonModule],
selector: 'app',
template: `
<ng-container *ngIf="true" i18n>Hi!</ng-container>
`,
})
class SimpleComponent {
}
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<div i18n-title title="Hello world">Hi!</div>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngh');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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('<app ngh');
});
});
it('should *not* throw when i18n is used in nested component ' +
'excluded using `ngSkipHydration`',
async () => {
@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: `
<div i18n>Hi!</div>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<div i18n>Hi!</div>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="true">');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<main *ngIf="true">
<div *ngIf="true" i18n>Hi!</div>
</main>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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 <ng-container>s',
async () => {
@Component({
standalone: true,
selector: 'app',
template: `
<ng-container i18n>Hi!</ng-container>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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 <ng-container>s)',
async () => {
@Component({
standalone: true,
imports: [CommonModule],
selector: 'app',
template: `
<ng-container *ngIf="true" i18n>Hi!</ng-container>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngskiphydration="">');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<div i18n-title title="Hello world">Hi!</div>
`,
})
class SimpleComponent {
}
const html = await ssr(SimpleComponent);
const ssrContents = getAppContents(html);
expect(ssrContents).toContain('<app ngh');
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<div i18n>Hi!</div>
`,
})
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:
<nested ngSkipHydration />
`,
})
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('<app ngh');
expect(ssrContents).toContain('<app ngh');
resetTViewsFor(SimpleComponent);
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(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: `
<div i18n>Hi!</div>
`,
})
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):
<nested />
`,
})
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('<app ngh');
expect(ssrContents).toContain('<app ngh');
resetTViewsFor(SimpleComponent);
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
const appRef = await hydrate(html, SimpleComponent);
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
const clientRootNode = compRef.location.nativeElement;
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
const clientRootNode = compRef.location.nativeElement;
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
});
});
});