-
Notifications
You must be signed in to change notification settings - Fork 27.2k
feat(core): support bootstrapping Angular components underneath shadow roots #66782
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
base: main
Are you sure you want to change the base?
Changes from all commits
8258e33
37574fd
38602a9
294d578
a1ba01e
cf94a8e
1ccb813
16c49c9
a89baf0
5b5a4f0
17f2628
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,13 @@ export interface RNode { | |
| * Used exclusively for building up DOM which are static (ie not View roots) | ||
| */ | ||
| appendChild(newChild: RNode): RNode; | ||
|
|
||
| /** | ||
| * Returns the root node (Document or ShadowRoot) of this node. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpicking, but this could return a node if called on an unattached node. |
||
| * | ||
| * @see https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode | ||
| */ | ||
| getRootNode?(): RNode | ShadowRoot | null; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When does this return null? |
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| import {InjectionToken} from '../../di/injection_token'; | ||
|
|
||
| /** Token used to retrieve the `SharedStylesHost`. */ | ||
| export const SHARED_STYLES_HOST = new InjectionToken<SharedStylesHost>( | ||
| typeof ngDevMode !== 'undefined' && ngDevMode ? 'SHARED_STYLES_HOST' : '', | ||
| ); | ||
|
|
||
| /** Manages stylesheets for components in the application. */ | ||
| export interface SharedStylesHost { | ||
| /** | ||
| * Adds embedded styles to the DOM via HTML `style` elements. | ||
| * @param styles An array of style content strings. | ||
| * @param urls An array of URLs to be added as link tags. | ||
| */ | ||
| addStyles(styles: string[], urls?: string[]): void; | ||
|
|
||
| /** | ||
| * Removes embedded styles from the DOM that were added as HTML `style` elements. | ||
| * @param styles An array of style content strings. | ||
| * @param urls An array of URLs to be removed as link tags. | ||
| */ | ||
| removeStyles(styles: string[], urls?: string[]): void; | ||
|
|
||
| /** | ||
| * Adds a host node to contain styles added to the DOM and adds all existing style usage to | ||
| * the newly added host node. | ||
| * | ||
| * @param hostNode The node to contain styles added to the DOM. | ||
| */ | ||
| addHost(hostNode: Node): void; | ||
|
|
||
| /** | ||
| * Removes a host node from the set of style hosts and removes all existing style usage from | ||
| * the removed host node. | ||
| * | ||
| * @param hostNode The node to remove from the set of style hosts. | ||
| */ | ||
| removeHost(hostNode: Node): void; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,10 @@ import { | |
| T_HOST, | ||
| TView, | ||
| TVIEW, | ||
| HOST, | ||
| ENVIRONMENT, | ||
| } from '../interfaces/view'; | ||
| import {getDocument} from '../interfaces/document'; | ||
| import { | ||
| addViewToDOM, | ||
| destroyLView, | ||
|
|
@@ -116,6 +119,16 @@ export function addLViewToLContainer( | |
| } | ||
| } | ||
|
|
||
| // To ensure styles are placed on a parent shadow root, we need to register it as a host. | ||
| const sharedStylesHost = lView[ENVIRONMENT].sharedStylesHost; | ||
| if (sharedStylesHost) { | ||
| const host = lView[HOST] ?? lContainer[NATIVE]; | ||
| const rootNode = host.getRootNode?.(); | ||
| const isShadowRoot = | ||
| rootNode && typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot; | ||
| sharedStylesHost.addHost(isShadowRoot ? rootNode : lView[ENVIRONMENT].fallbackDocument.head); | ||
| } | ||
|
|
||
| // When in hydration mode, reset the pointer to the first child in | ||
| // the dehydrated view. This indicates that the view was hydrated and | ||
| // further attaching/detaching should work with this view as normal. | ||
|
|
@@ -153,6 +166,20 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView | | |
| const viewToDetach = lContainer[indexInContainer]; | ||
|
|
||
| if (viewToDetach) { | ||
| const host = viewToDetach[HOST] ?? lContainer[NATIVE]; | ||
| if (host.isConnected) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AGENT:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can keep a WeakMap of LView->Node and just grab the host that way?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We don't check There is an I think I'll add an assertion here that
In what scenario would this happen? If the user is manually grabbing an element and removing it from the DOM without going through Angular, I feel like a lot of other things would break before they started caring about leaking styles. Is this a supported use cases elsewhere?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dgp1130 People do manual DOM manipulation a lot more often than we'd like, unfortunately. |
||
| const sharedStylesHost = viewToDetach[ENVIRONMENT].sharedStylesHost; | ||
| if (sharedStylesHost) { | ||
| // Undo the `SharedStylesHost` registration. | ||
| const rootNode = host.getRootNode?.(); | ||
| const isShadowRoot = | ||
| rootNode && typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot; | ||
| sharedStylesHost.removeHost( | ||
| isShadowRoot ? rootNode : viewToDetach[ENVIRONMENT].fallbackDocument.head, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| const declarationLContainer = viewToDetach[DECLARATION_LCONTAINER]; | ||
| if (declarationLContainer !== null && declarationLContainer !== lContainer) { | ||
| detachMovedView(declarationLContainer, viewToDetach); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this be null or will it be the hostElement itself? Looking at the getRootNode docs on MDN, I'd assume it'd return the element itself?