Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): add hydration protected elements #52478

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion goldens/circular-deps/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,4 @@
"packages/router/src/shared.ts",
"packages/router/src/url_tree.ts"
]
]
]
4 changes: 4 additions & 0 deletions packages/core/src/hydration/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import {enableApplyRootElementTransformImpl} from '../render3/instructions/share
import {enableLocateOrCreateContainerAnchorImpl} from '../render3/instructions/template';
import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text';
import {getDocument} from '../render3/interfaces/document';
import {enableSetAttributeWithHydrationSupportImpl} from '../render3/util/attrs_utils_with_hydration_support';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {enableSetPropertyWithHydrationSupportImpl} from '../render3/util/prop_utils_with_hydration_support';
import {TransferState} from '../transfer_state';
import {performanceMarkFeature} from '../util/performance';
import {NgZone} from '../zone';
Expand Down Expand Up @@ -89,6 +91,8 @@ function enableHydrationRuntimeSupport() {
enableLocateOrCreateContainerRefImpl();
enableFindMatchingDehydratedViewImpl();
enableApplyRootElementTransformImpl();
enableSetAttributeWithHydrationSupportImpl();
enableSetPropertyWithHydrationSupportImpl();
}
}

Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/hydration/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ export interface DehydratedView {
* removed from the DOM during hydration cleanup.
*/
dehydratedIcuData?: Map<number, DehydratedIcuData>;

/**
* A mapping from the node (TNode) index to a hydration protected attribute value.
*
* This information is utilized during the hydration process on the client when the initial render
* occurs (creation mode). Its purpose is to avoid overwriting attributes if they are already set
* on the element on the server side. For instance, the `iframe` element may already be rendered
* with the `src` attribute set. However, during the hydration process, calling `setAttribute`
* again could cause the `iframe` to reload the resource, even if the `src` attribute value
* remains the same as it was set on the server.
*/
protectedAttributes?: Map<number, string | null>;
}

/**
Expand Down
25 changes: 21 additions & 4 deletions packages/core/src/render3/component_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ function createRootComponentView(
hostRenderer: Renderer,
): LView {
const tView = rootView[TVIEW];
applyRootComponentStyling(rootDirectives, tNode, hostRNode, hostRenderer);
applyRootComponentStyling(rootDirectives, tNode, hostRNode, hostRenderer, rootView);

// Hydration info is on the host element and needs to be retrieved
// and passed to the component LView.
Expand Down Expand Up @@ -564,6 +564,7 @@ function applyRootComponentStyling(
tNode: TElementNode,
rNode: RElement | null,
hostRenderer: Renderer,
lView: LView,
): void {
for (const def of rootDirectives) {
tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, def.hostAttrs);
Expand All @@ -573,7 +574,7 @@ function applyRootComponentStyling(
computeStaticStyling(tNode, tNode.mergedAttrs, true);

if (rNode !== null) {
setupStaticAttributes(hostRenderer, rNode, tNode);
setupStaticAttributes(lView, tNode, hostRenderer, rNode);
}
}
}
Expand Down Expand Up @@ -643,14 +644,30 @@ function setRootNodeAttributes(
) {
if (rootSelectorOrNode) {
// The placeholder will be replaced with the actual version at build time.
setUpAttributes(hostRenderer, hostRNode, ['ng-version', '0.0.0-PLACEHOLDER']);
setUpAttributes(
// Note that the following arguments are not provided because the root element and its
// attributes do not require special handling during hydration.
/* lView */ null,
/* tNode */ null,
hostRenderer,
hostRNode,
['ng-version', '0.0.0-PLACEHOLDER'],
);
} else {
// If host element is created as a part of this function call (i.e. `rootSelectorOrNode`
// is not defined), also apply attributes and classes extracted from component selector.
// Extract attributes and classes from the first selector only to match VE behavior.
const {attrs, classes} = extractAttrsAndClassesFromSelector(componentDef.selectors[0]);
if (attrs) {
setUpAttributes(hostRenderer, hostRNode, attrs);
setUpAttributes(
// Note that the following arguments are not provided because the root element and its
// attributes do not require special handling during hydration.
/* lView */ null,
/* tNode */ null,
hostRenderer,
hostRNode,
attrs,
);
}
if (classes && classes.length > 0) {
writeDirectClass(hostRenderer, hostRNode, classes.join(' '));
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/render3/i18n/i18n_apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ export function applyMutableOpCodes(
// This code is used for ICU expressions only, since we don't support
// directives/components in ICUs, we don't need to worry about inputs here
setElementAttribute(
// Note that the following arguments are not provided because the i18n stuff and its
// attributes do not require special handling during hydration.
/* lView */ null,
/* tNode */ null,
renderer,
getNativeByIndex(elementNodeIndex, lView) as RElement,
null,
Expand Down Expand Up @@ -434,6 +438,10 @@ export function applyUpdateOpCodes(
// not have TNode), in which case we know that there are no directives, and hence
// we use attribute setting.
setElementAttribute(
// Note that the following arguments are not provided because the i18n stuff and its
// attributes do not require special handling during hydration.
/* lView */ null,
/* tNode */ null,
lView[RENDERER],
lView[nodeIndex],
null,
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/render3/instructions/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
TAttributes,
TElementNode,
TNode,
TNodeFlags,
TNodeType,
} from '../interfaces/node';
import {Renderer} from '../interfaces/renderer';
Expand Down Expand Up @@ -68,6 +67,7 @@ import {
wasLastNodeCreated,
} from '../state';
import {computeStaticStyling} from '../styling/static_styling';
import {getHydrationProtectedAttribute} from '../util/protected_attributes';
import {getConstant} from '../util/view_utils';

import {validateElementIsKnown} from './element_validation';
Expand Down Expand Up @@ -160,7 +160,7 @@ export function ɵɵelementStart(
}

setCurrentTNode(tNode, true);
setupStaticAttributes(renderer, native, tNode);
setupStaticAttributes(lView, tNode, renderer, native);

if (!isDetachedByI18n(tNode) && wasLastNodeCreated()) {
// In the i18n case, the translation may have removed this element, so only add it if it is not
Expand Down Expand Up @@ -294,6 +294,20 @@ function locateOrCreateElementNodeImpl(
ngDevMode && validateMatchingNode(native, Node.ELEMENT_NODE, name, lView, tNode);
ngDevMode && markRNodeAsClaimedByHydration(native);

const hydrationProtectedAttribute = getHydrationProtectedAttribute(native.tagName);
// If we're examining an element that is in the list of hydration-protected elements (e.g.,
// `<iframe>`) and it has a set attribute (e.g., `src`).
if (hydrationProtectedAttribute && native.hasAttribute(hydrationProtectedAttribute)) {
// Note that the attribute may be set through attribute bindings, for example,
// `[attr.src]`, or through property binding, i.e., `[src]="url"`. In both cases,
// even if the property is set on the element (`el[name] = value`), it will also set
// an attribute. Since we're in hydration mode, the attribute should be set on the
// server, and we're safe to check whether it has an attribute set.
const attributeValue = native.getAttribute(hydrationProtectedAttribute);
const protectedAttributes = (hydrationInfo.protectedAttributes ??= new Map<number, string>());
protectedAttributes.set(tNode.index, attributeValue);
}

// This element might also be an anchor of a view container.
if (getSerializedContainerViews(hydrationInfo, index)) {
// Important note: this element acts as an anchor, but it's **not** a part
Expand Down
21 changes: 17 additions & 4 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
import {escapeCommentText} from '../../util/dom';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
import {stringify} from '../../util/stringify';
import {applyValueToInputField} from '../apply_value_input_field';
import {
assertFirstCreatePass,
assertFirstUpdatePass,
Expand Down Expand Up @@ -142,6 +141,7 @@ import {
} from '../state';
import {NO_CHANGE} from '../tokens';
import {mergeHostAttrs} from '../util/attrs_utils';
import {_setAttributeImpl} from '../util/attrs_utils_with_hydration_support';
import {INTERPOLATION_DELIMITER} from '../util/misc_utils';
import {renderStringify} from '../util/stringify_utils';
import {
Expand All @@ -156,6 +156,7 @@ import {selectIndexInternal} from './advance';
import {ɵɵdirectiveInject} from './di';
import {handleUnknownPropertyError, isPropertyValid, matchingSchemas} from './element_validation';
import {writeToDirectiveInput} from './write_to_directive_input';
import {_setPropertyImpl} from '../util/prop_utils_with_hydration_support';

/**
* Invoke `HostBindingsFunction`s for view.
Expand Down Expand Up @@ -1139,7 +1140,7 @@ export function elementPropertyInternal<T>(
// It is assumed that the sanitizer is only added when the compiler determines that the
// property is risky, so sanitization can be done without further checks.
value = sanitizer != null ? (sanitizer(value, tNode.value || '', propName) as any) : value;
renderer.setProperty(element as RElement, propName, value);
_setPropertyImpl(lView, tNode, renderer, element as RElement, propName, value);
} else if (tNode.type & TNodeType.AnyContainer) {
// If the node is a container and the property didn't
// match any of the inputs or schemas we should throw.
Expand Down Expand Up @@ -1694,10 +1695,22 @@ export function elementAttributeInternal(
);
}
const element = getNativeByTNode(tNode, lView) as RElement;
setElementAttribute(lView[RENDERER], element, namespace, tNode.value, name, value, sanitizer);
setElementAttribute(
lView,
tNode,
lView[RENDERER],
element,
namespace,
tNode.value,
name,
value,
sanitizer,
);
}

export function setElementAttribute(
lView: LView | null,
tNode: TNode | null,
renderer: Renderer,
element: RElement,
namespace: string | null | undefined,
Expand All @@ -1714,7 +1727,7 @@ export function setElementAttribute(
const strValue =
sanitizer == null ? renderStringify(value) : sanitizer(value, tagName || '', name);

renderer.setAttribute(element, name, strValue as string, namespace);
_setAttributeImpl(lView, tNode, renderer, element, name, strValue, namespace);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render3/interfaces/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {InputSignalNode} from '../../authoring/input/input_signal_node';
import {ModuleWithProviders, ProcessProvidersFunction} from '../../di/interface/provider';
import {EnvironmentInjector} from '../../di/r3_injector';
import type {EnvironmentInjector} from '../../di/r3_injector';
import {Type} from '../../interface/type';
import {SchemaMetadata} from '../../metadata/schema';
import {ViewEncapsulation} from '../../metadata/view';
Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/render3/node_manipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {
consumerDestroy,
getActiveConsumer,
setActiveConsumer,
} from '@angular/core/primitives/signals';
import {consumerDestroy, setActiveConsumer} from '@angular/core/primitives/signals';

import {NotificationSource} from '../change_detection/scheduling/zoneless_scheduling';
import {hasInSkipHydrationBlockFlag} from '../hydration/skip_hydration';
Expand Down Expand Up @@ -1284,11 +1280,16 @@ export function writeDirectClass(renderer: Renderer, element: RElement, newValue
}

/** Sets up the static DOM attributes on an `RNode`. */
export function setupStaticAttributes(renderer: Renderer, element: RElement, tNode: TNode) {
export function setupStaticAttributes(
lView: LView,
tNode: TNode,
renderer: Renderer,
element: RElement,
) {
const {mergedAttrs, classes, styles} = tNode;

if (mergedAttrs !== null) {
setUpAttributes(renderer, element, mergedAttrs);
setUpAttributes(lView, tNode, renderer, element, mergedAttrs);
}

if (classes !== null) {
Expand Down
23 changes: 17 additions & 6 deletions packages/core/src/render3/util/attrs_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
*/
import {CharCode} from '../../util/char_code';
import {AttributeMarker} from '../interfaces/attribute_marker';
import {TAttributes} from '../interfaces/node';
import {CssSelector} from '../interfaces/projection';
import {Renderer} from '../interfaces/renderer';
import {RElement} from '../interfaces/renderer_dom';
import type {TAttributes, TNode} from '../interfaces/node';
import type {CssSelector} from '../interfaces/projection';
import type {Renderer} from '../interfaces/renderer';
import type {RElement} from '../interfaces/renderer_dom';
import type {LView} from '../interfaces/view';

import {_setAttributeImpl} from './attrs_utils_with_hydration_support';

/**
* Assigns all attribute values to the provided element via the inferred renderer.
Expand All @@ -34,12 +37,20 @@ import {RElement} from '../interfaces/renderer_dom';
* Note that this instruction does not support assigning style and class values to
* an element. See `elementStart` and `elementHostAttrs` to learn how styling values
* are applied to an element.
* @param lView the LView of the current TNode
* @param tNode The node for which we're setting attributes on
* @param renderer The renderer to be used
* @param native The element that the attributes will be assigned to
* @param attrs The attribute array of values that will be assigned to the element
* @returns the index value that was last accessed in the attributes array
*/
export function setUpAttributes(renderer: Renderer, native: RElement, attrs: TAttributes): number {
export function setUpAttributes(
lView: LView | null,
tNode: TNode | null,
renderer: Renderer,
native: RElement,
attrs: TAttributes,
): number {
let i = 0;
while (i < attrs.length) {
const value = attrs[i];
Expand Down Expand Up @@ -68,7 +79,7 @@ export function setUpAttributes(renderer: Renderer, native: RElement, attrs: TAt
if (isAnimationProp(attrName)) {
renderer.setProperty(native, attrName, attrVal);
} else {
renderer.setAttribute(native, attrName, attrVal as string);
_setAttributeImpl(lView, tNode, renderer, native, attrName, attrVal as string);
}
i++;
}
Expand Down
Loading