Skip to content
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
5 changes: 2 additions & 3 deletions packages/core/src/hydration/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {getComponentDef} from '../render3/definition';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
import {TNode, TNodeType} from '../render3/interfaces/node';
import {RElement} from '../render3/interfaces/renderer_dom';
import {isComponentHost, isLContainer, isProjectionTNode, isRootView} from '../render3/interfaces/type_checks';
import {hasI18n, isComponentHost, isLContainer, isProjectionTNode, isRootView} from '../render3/interfaces/type_checks';
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
import {unwrapRNode} from '../render3/util/view_utils';
import {TransferState} from '../transfer_state';
Expand Down Expand Up @@ -412,8 +412,7 @@ function componentUsesShadowDomEncapsulation(lView: LView): boolean {
function annotateHostElementForHydration(
element: RElement, lView: LView, context: HydrationContext): void {
const renderer = lView[RENDERER];
if ((lView[FLAGS] & LViewFlags.HasI18n) === LViewFlags.HasI18n ||
componentUsesShadowDomEncapsulation(lView)) {
if (hasI18n(lView) || 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
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/hydration/skip_hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {TNode, TNodeFlags} from '../render3/interfaces/node';
import {LView} from '../render3/interfaces/view';
import {RElement} from '../render3/interfaces/renderer_dom';

/**
* The name of an attribute that can be added to the hydration boundary node
Expand All @@ -16,9 +16,9 @@ import {LView} from '../render3/interfaces/view';
export const SKIP_HYDRATION_ATTR_NAME = 'ngSkipHydration';

/**
* Helper function to check if a given node has the 'ngSkipHydration' attribute
* Helper function to check if a given TNode has the 'ngSkipHydration' attribute.
*/
export function hasNgSkipHydrationAttr(tNode: TNode): boolean {
export function hasSkipHydrationAttrOnTNode(tNode: TNode): boolean {
const SKIP_HYDRATION_ATTR_NAME_LOWER_CASE = SKIP_HYDRATION_ATTR_NAME.toLowerCase();

const attrs = tNode.mergedAttrs;
Expand All @@ -36,6 +36,13 @@ export function hasNgSkipHydrationAttr(tNode: TNode): boolean {
return false;
}

/**
* Helper function to check if a given RElement has the 'ngSkipHydration' attribute.
*/
export function hasSkipHydrationAttrOnRElement(rNode: RElement): boolean {
return rNode.hasAttribute(SKIP_HYDRATION_ATTR_NAME);
}

/**
* Checks whether a TNode has a flag to indicate that it's a part of
* a skip hydration block.
Expand All @@ -56,7 +63,7 @@ export function hasInSkipHydrationBlockFlag(tNode: TNode): boolean {
export function isInSkipHydrationBlock(tNode: TNode): boolean {
let currentTNode: TNode|null = tNode.parent;
while (currentTNode) {
if (hasNgSkipHydrationAttr(currentTNode)) {
if (hasSkipHydrationAttrOnTNode(currentTNode)) {
return true;
}
currentTNode = currentTNode.parent;
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/render3/instructions/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {invalidSkipHydrationHost, validateMatchingNode, validateNodeExists} from '../../hydration/error_handling';
import {locateNextRNode} from '../../hydration/node_lookup_utils';
import {hasNgSkipHydrationAttr} from '../../hydration/skip_hydration';
import {hasSkipHydrationAttrOnRElement, hasSkipHydrationAttrOnTNode} from '../../hydration/skip_hydration';
import {getSerializedContainerViews, isDisconnectedNode, markRNodeAsClaimedByHydration, setSegmentHead} from '../../hydration/utils';
import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert';
import {assertFirstCreatePass, assertHasParent} from '../assert';
Expand All @@ -17,7 +17,7 @@ import {registerPostOrderHooks} from '../hooks';
import {hasClassInput, hasStyleInput, TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {Renderer} from '../interfaces/renderer';
import {RElement} from '../interfaces/renderer_dom';
import {isComponentHost, isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks';
import {hasI18n, isComponentHost, isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks';
import {HEADER_OFFSET, HYDRATION, LView, RENDERER, TView} from '../interfaces/view';
import {assertTNodeType} from '../node_assert';
import {appendChild, clearElementContents, createElementNode, setupStaticAttributes} from '../node_manipulation';
Expand Down Expand Up @@ -230,8 +230,11 @@ function locateOrCreateElementNodeImpl(
}

// Checks if the skip hydration attribute is present during hydration so we know to
// skip attempting to hydrate this block.
if (hydrationInfo && hasNgSkipHydrationAttr(tNode)) {
// skip attempting to hydrate this block. We check both TNode and RElement for an
// attribute: the RElement case is needed for i18n cases, when we add it to host
// elements during the annotation phase (after all internal data structures are setup).
if (hydrationInfo &&
(hasSkipHydrationAttrOnTNode(tNode) || hasSkipHydrationAttrOnRElement(native))) {
if (isComponentHost(tNode)) {
enterSkipHydrationBlock(tNode);

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {Injector} from '../../di/injector';
import {ErrorHandler} from '../../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {DehydratedView} from '../../hydration/interfaces';
import {hasInSkipHydrationBlockFlag, SKIP_HYDRATION_ATTR_NAME} from '../../hydration/skip_hydration';
import {hasInSkipHydrationBlockFlag, hasSkipHydrationAttrOnRElement, SKIP_HYDRATION_ATTR_NAME} from '../../hydration/skip_hydration';
import {PRESERVE_HOST_CONTENT, PRESERVE_HOST_CONTENT_DEFAULT} from '../../hydration/tokens';
import {processTextNodeMarkersBeforeHydration} from '../../hydration/utils';
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
Expand Down Expand Up @@ -497,7 +497,7 @@ let _applyRootElementTransformImpl: typeof applyRootElementTransformImpl =
* @param rootElement the app root HTML Element
*/
export function applyRootElementTransformImpl(rootElement: HTMLElement) {
if (rootElement.hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
if (hasSkipHydrationAttrOnRElement(rootElement)) {
// Handle a situation when the `ngSkipHydration` attribute is applied
// to the root node of an application. In this case, we should clear
// the contents and render everything from scratch.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/interfaces/renderer_dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface RElement extends RNode {
className: string;
tagName: string;
textContent: string|null;
hasAttribute(name: string): boolean;
getAttribute(name: string): string|null;
setAttribute(name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void;
removeAttribute(name: string): void;
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/render3/interfaces/type_checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ export function isRootView(target: LView): boolean {
export function isProjectionTNode(tNode: TNode): boolean {
return (tNode.type & TNodeType.Projection) === TNodeType.Projection;
}

export function hasI18n(lView: LView): boolean {
return (lView[FLAGS] & LViewFlags.HasI18n) === LViewFlags.HasI18n;
}
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,9 @@
{
"name": "hasInSkipHydrationBlockFlag"
},
{
"name": "hasSkipHydrationAttrOnRElement"
},
{
"name": "hostReportError"
},
Expand Down
40 changes: 40 additions & 0 deletions packages/platform-server/test/hydration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,46 @@ describe('platform-server hydration integration', () => {
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
});

it('should exclude components with i18n from hydration automatically', async () => {
@Component({
standalone: true,
selector: 'nested',
template: `
<div i18n>Hi!</div>
`,
})
class NestedComponent {
}

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

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

describe('ShadowDom encapsulation', () => {
Expand Down