Skip to content
Open
6 changes: 6 additions & 0 deletions goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,7 @@ export function reflectComponentType<C>(component: Type<C>): ComponentMirror<C>
export abstract class Renderer2 {
abstract addClass(el: any, name: string): void;
abstract appendChild(parent: any, newChild: any): void;
abstract applyStyles?(styleRoot: StyleRoot): void;
abstract createComment(value: string): any;
abstract createElement(name: string, namespace?: string | null): any;
abstract createText(value: string): any;
Expand All @@ -1579,11 +1580,13 @@ export abstract class Renderer2 {
abstract removeChild(parent: any, oldChild: any, isHostElement?: boolean, requireSynchronousElementRemoval?: boolean): void;
abstract removeClass(el: any, name: string): void;
abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void;
abstract removeStyles?(styleRoot: StyleRoot): void;
abstract selectRootElement(selectorOrNode: string | any, preserveContent?: boolean): any;
abstract setAttribute(el: any, name: string, value: string, namespace?: string | null): void;
abstract setProperty(el: any, name: string, value: any): void;
abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void;
abstract setValue(node: any, value: string): void;
shadowRoot?: ShadowRoot;
}

// @public
Expand Down Expand Up @@ -1829,6 +1832,9 @@ export interface StreamingResourceOptions<T, R> extends BaseResourceOptions<T, R
stream: ResourceStreamingLoader<T, R>;
}

// @public
export type StyleRoot = Document | ShadowRoot;

// @public
export class TemplateRef<C> {
createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef<C>;
Expand Down
15 changes: 14 additions & 1 deletion packages/animations/browser/src/render/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,30 @@ export class BaseAnimationRenderer implements Renderer2 {
// See https://github.com/microsoft/rushstack/issues/4390
readonly ɵtype: AnimationRendererType.Regular = AnimationRendererType.Regular;

applyStyles: Renderer2['applyStyles'];
removeStyles: Renderer2['removeStyles'];

constructor(
protected namespaceId: string,
public delegate: Renderer2,
public engine: AnimationEngine,
private _onDestroy?: () => void,
) {}
) {
// Conditionally define styling functionality based on the delegate, so we don't
// always implement `applyStyles` / `removeStyles` but then no-op if the delegate
// doesn't support it.
this.applyStyles = delegate.applyStyles?.bind(delegate);
this.removeStyles = delegate.removeStyles?.bind(delegate);
}

get data() {
return this.delegate.data;
}

get shadowRoot() {
return this.delegate.shadowRoot;
}

destroyNode(node: any): void {
this.delegate.destroyNode?.(node);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/animation/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function addToAnimationQueue(
// is re-attached before the animation queue runs.
animationData?.detachedLeaveAnimationFns?.push(animationFns);
}
console.log(`Scheduling: ${Boolean(animationQueue.scheduler)}`); // DEBUG
animationQueue.scheduler && animationQueue.scheduler(injector);
}

Expand All @@ -72,8 +73,10 @@ export function scheduleAnimationQueue(injector: Injector) {
const animationQueue = injector.get(ANIMATION_QUEUE);
// We only want to schedule the animation queue if it hasn't already been scheduled.
if (!animationQueue.isScheduled) {
console.log('afterNextRender: scheduled'); // DEBUG
afterNextRender(
() => {
console.log('afterNextRender: running'); // DEBUG
animationQueue.isScheduled = false;
for (let animateFn of animationQueue.queue) {
animateFn();
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
*/

// Public API for render
export {Renderer2, RendererFactory2, ListenerOptions} from './render/api';
export {Renderer2, RendererFactory2, ListenerOptions, StyleRoot} from './render/api';
export {RendererStyleFlags2, RendererType2} from './render/api_flags';
46 changes: 46 additions & 0 deletions packages/core/src/render/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,35 @@ import {getComponentLViewByIndex} from '../render3/util/view_utils';

import {RendererStyleFlags2, RendererType2} from './api_flags';

/**
* A location where CSS stylesheets may be added.
*
* @publicApi
*/
export type StyleRoot = Document | ShadowRoot;

/**
* Asserts that the given node is a {@link StyleRoot}. Useful for converting the return value of
* {@link Node.prototype.getRootNode} into a {@link StyleRoot}. If the root is a detached node,
* this returns `undefined` as there is no meaningful style root to attach styles to.
*
* @param root The root node of a DOM tree to convert to a {@link StyleRoot}.
* @returns The given root as a {@link StyleRoot} if it is a valid root for styles. Otherwise
* returns `undefined`.
*/
export function asStyleRoot(root: Node): StyleRoot | undefined {
// Need to feature-detect `ShadowRoot` for Node environments where DOM emulation does not
// support it.
if (
root instanceof Document ||
(typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot)
) {
return root;
}

return undefined;
}

/**
* Creates and initializes a custom renderer that implements the `Renderer2` base class.
*
Expand Down Expand Up @@ -241,6 +270,23 @@ export abstract class Renderer2 {
options?: ListenerOptions,
): () => void;

/** The component's internal shadow root if one is used. */
shadowRoot?: ShadowRoot;

/**
* Attach any required stylesheets for the associated component to the provided
* {@link StyleRoot}. This is called when the component is initially rendered
* xor attached to a view.
*/
abstract applyStyles?(styleRoot: StyleRoot): void;

/**
* Detach any required stylesheets for the associated component from the
* provided {@link StyleRoot}. This is called when the component is destroyed
* xor detached from a view.
*/
abstract removeStyles?(styleRoot: StyleRoot): void;

/**
* @internal
* @nocollapse
Expand Down
36 changes: 35 additions & 1 deletion packages/core/src/render3/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
RENDERER,
T_HOST,
TVIEW,
TViewType,
} from './interfaces/view';
import {assertTNodeType} from './node_assert';
import {destroyLView, removeViewFromDOM} from './node_manipulation';
Expand All @@ -44,6 +45,7 @@ import {
getInitialLViewFlagsFromDef,
getOrCreateComponentTView,
} from './view/construction';
import {getStyleRoot, walkDescendants} from './util/view_traversal_utils';

/** Represents `import.meta` plus some information that's not in the built-in types. */
type ImportMetaExtended = ImportMeta & {
Expand Down Expand Up @@ -241,6 +243,16 @@ function recreateLView(
ngDevMode && assertNotEqual(newDef, oldDef, 'Expected different component definition');
const zone = lView[INJECTOR].get(NgZone, null);
const recreate = () => {
// Remove old styles to make sure we drop internal references and track usage
// counts correctly. We might be an `Emulated` or `None` component inside a
// shadow root.
const oldRenderer = lView[RENDERER];
if (oldRenderer.removeStyles) {
const oldStyleRoot = getStyleRoot(lView);
ngDevMode && assertDefined(oldStyleRoot, 'oldStyleRoot');
oldRenderer.removeStyles(oldStyleRoot!);
}

// If we're recreating a component with shadow DOM encapsulation, it will have attached a
// shadow root. The browser will throw if we attempt to attach another one and there's no way
// to detach it. Our only option is to make a clone only of the root node, replace the node
Expand All @@ -249,6 +261,17 @@ function recreateLView(
oldDef.encapsulation === ViewEncapsulation.ShadowDom ||
oldDef.encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom
) {
// Remove all descendants' styles because they will be destroyed with this
// shadow root.
for (const view of walkDescendants(lView)) {
if (isLContainer(view) || view[TVIEW].type !== TViewType.Component) continue;

const styleRoot = getStyleRoot(view);
ngDevMode && assertDefined(styleRoot, 'styleRoot');
const renderer = view[RENDERER];
renderer.removeStyles?.(styleRoot!);
}

const newHost = host.cloneNode(false) as HTMLElement;
host.replaceWith(newHost);
host = newHost;
Expand Down Expand Up @@ -286,7 +309,18 @@ function recreateLView(

// Patch a brand-new renderer onto the new view only after the old
// view is destroyed so that the runtime doesn't try to reuse it.
newLView[RENDERER] = rendererFactory.createRenderer(host, newDef);
const newRenderer = rendererFactory.createRenderer(host, newDef);
newLView[RENDERER] = newRenderer;

// Reapply styles potentially with a newly created shadow root.
if (newRenderer.applyStyles) {
const newStyleRoot = getStyleRoot(newLView);
ngDevMode && assertDefined(newStyleRoot, 'newStyleRoot');
newRenderer.applyStyles(newStyleRoot!);

// Don't need to reapply styles for descendent components because
// that will be handled as part of rendering the view.
}

// Remove the nodes associated with the destroyed LView. This removes the
// descendants, but not the host which we want to stay in place.
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/render3/instructions/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ function runLeaveAnimations(

const renderer = lView[RENDERER];
const ngZone = lView[INJECTOR].get(NgZone);
console.log(`runLeaveAnimations scheduling: ${lView[ID]}\n${new Error().stack}`); // DEBUG
allLeavingAnimations.add(lView[ID]);
(getLViewLeaveAnimations(lView).get(tNode.index)!.resolvers ??= []).push(resolve);

Expand Down Expand Up @@ -389,6 +390,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
const tNode = getCurrentTNode()!;
cancelLeavingNodes(tNode, lView);

console.log(`ɵɵanimateLeaveListener, scheduling: ${lView[ID]}`); // DEBUG
allLeavingAnimations.add(lView[ID]);

addAnimationToLView(getLViewLeaveAnimations(lView), tNode, () =>
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/render3/instructions/control_flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,11 @@ function clearDetachAnimationList(lContainer: LContainer, index: number): void {
const injector = viewToDetach[INJECTOR];
removeFromAnimationQueue(injector, animations);
allLeavingAnimations.delete(viewToDetach[ID]);
console.log(`clearDetachAnimationList: Removing animation: ${viewToDetach[ID]}`); // DEBUG
animations.detachedLeaveAnimationFns = undefined;
} else {
allLeavingAnimations.delete(viewToDetach[ID]);
console.log(`clearDetachAnimationList: Not removing animation: ${viewToDetach?.[ID]}`); // DEBUG
}
}

Expand Down
18 changes: 17 additions & 1 deletion packages/core/src/render3/instructions/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {retrieveHydrationInfo} from '../../hydration/utils';
import {assertEqual, assertNotReactive} from '../../util/assert';
import {assertDefined, assertEqual, assertNotReactive} from '../../util/assert';
import {RenderFlags} from '../interfaces/definition';
import {
CONTEXT,
Expand All @@ -18,6 +18,7 @@ import {
LView,
LViewFlags,
QUERIES,
RENDERER,
TVIEW,
TView,
} from '../interfaces/view';
Expand All @@ -28,6 +29,7 @@ import {enterView, leaveView} from '../state';
import {getComponentLViewByIndex, isCreationMode} from '../util/view_utils';

import {executeTemplate} from './shared';
import {getStyleRoot} from '../util/view_traversal_utils';

export function renderComponent(hostLView: LView, componentHostIdx: number) {
ngDevMode && assertEqual(isCreationMode(hostLView), true, 'Should be run in creation mode');
Expand All @@ -45,6 +47,20 @@ export function renderComponent(hostLView: LView, componentHostIdx: number) {

try {
renderView(componentTView, componentView, componentView[CONTEXT]);

// TODO: test
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a reminder

if (hostRNode?.isConnected) {
// Element is already attached to the DOM, apply its styles immediately.
ngDevMode && assertDefined(componentView[CONTEXT], 'component instance');
const styleRoot = getStyleRoot(componentView);
ngDevMode && assertDefined(styleRoot, 'styleRoot');

const componentRenderer = componentView[RENDERER];
componentRenderer.applyStyles?.(styleRoot!);
} else {
// Element is *not* attached to the DOM, can't know where its styles should go.
// Styles will be applied when attaching the view to a container.
}
} finally {
profiler(ProfilerEvent.ComponentEnd, componentView[CONTEXT] as any as {});
}
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/render3/interfaces/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {RendererStyleFlags2, RendererType2} from '../../render/api_flags';
import type {ListenerOptions} from '../../render/api';
import type {ListenerOptions, StyleRoot} from '../../render/api';
import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../../util/security/trusted_type_defs';

import {RComment, RElement, RNode, RText} from './renderer_dom';
Expand Down Expand Up @@ -77,6 +77,15 @@ export interface Renderer {
callback: (event: any) => boolean | void,
options?: ListenerOptions,
): () => void;

/** The component's internal shadow root if one is used. */
shadowRoot?: ShadowRoot;

/** Attach any required stylesheets to the DOM. */
applyStyles?(styleRoot: StyleRoot): void;

/** Detach any stylesheets from the DOM. */
removeStyles?(styleRoot: StyleRoot): void;
}

export interface RendererFactory {
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/render3/interfaces/renderer_dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ export interface RNode {
* Used exclusively for building up DOM which are static (ie not View roots)
*/
appendChild(newChild: RNode): RNode;

/**
* Get the root node of the DOM tree containing the node.
*
* This is typically either the `document` object (when the Node exists in light
* DOM) or a `ShadowRoot` (when the Node exists in shadow DOM).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode
*/
getRootNode(options?: GetRootNodeOptions): Node;

/**
* Whether or not the Node is attached to a `Document` (directly or indirectly).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected
*/
readonly isConnected: boolean;
}

/**
Expand All @@ -78,6 +95,7 @@ export interface RElement extends RNode {
removeEventListener(type: string, listener?: EventListener, options?: boolean): void;
remove(): void;
setProperty?(name: string, value: any): void;
shadowRoot: ShadowRoot | null;
}

export interface RCssStyleDeclaration {
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/render3/node_manipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export function detachViewFromDOM(tView: TView, lView: LView) {
* - Destroy only called on movement to sibling or movement to parent (laterally or up)
*
* @param rootView The view to destroy
* TODO: Relevant?
*/
export function destroyViewTree(rootView: LView): void {
// If the view has no children, we can clean it up and return early.
Expand Down Expand Up @@ -388,7 +389,10 @@ function runLeaveAnimationsWithCallback(
if (animations == null || animations.leave == undefined || !animations.leave.has(tNode.index))
return callback(false);

if (lView) allLeavingAnimations.add(lView[ID]);
if (lView) {
console.log(`runLeaveAnimationsWithCallback scheduling: ${lView[ID]}\n${new Error().stack}`); // DEBUG
allLeavingAnimations.add(lView[ID]);
}

addToAnimationQueue(
injector,
Expand All @@ -411,7 +415,11 @@ function runLeaveAnimationsWithCallback(
animations.running = Promise.allSettled(runningAnimations);
runAfterLeaveAnimations(lView!, callback);
} else {
if (lView) allLeavingAnimations.delete(lView[ID]);
if (lView) {
allLeavingAnimations.delete(lView[ID]);
} else {
console.log('runAfterLeaveAnimations: Not deleting.'); // DEBUG
}
callback(false);
}
},
Expand All @@ -425,9 +433,12 @@ function runAfterLeaveAnimations(lView: LView, callback: Function) {
runningAnimations.then(() => {
lView[ANIMATIONS]!.running = undefined;
allLeavingAnimations.delete(lView[ID]);
console.log(`runAfterLeaveAnimations: Deleting: ${lView[ID]}`); // DEBUG
callback(true);
});
return;
} else {
console.log(`runAfterLeaveAnimations: Not deleting: ${lView[ID]}`); // DEBUG
}
callback(false);
}
Expand Down
Loading
Loading