diff --git a/adev/src/content/guide/routing/data-resolvers.md b/adev/src/content/guide/routing/data-resolvers.md index da83b8b1b2b5..60c79a676ab3 100644 --- a/adev/src/content/guide/routing/data-resolvers.md +++ b/adev/src/content/guide/routing/data-resolvers.md @@ -205,11 +205,11 @@ export class App { map(event => { if (event instanceof NavigationError) { this.lastFailedUrl.set(event.url); - + if (event.error) { console.error('Navigation error', event.error) } - + return 'Navigation failed. Please try again.'; } return ''; diff --git a/goldens/public-api/compiler-cli/error_code.api.md b/goldens/public-api/compiler-cli/error_code.api.md index b4005fd369ae..fa21b4feca06 100644 --- a/goldens/public-api/compiler-cli/error_code.api.md +++ b/goldens/public-api/compiler-cli/error_code.api.md @@ -78,6 +78,7 @@ export enum ErrorCode { INLINE_TYPE_CTOR_REQUIRED = 8901, INTERPOLATED_SIGNAL_NOT_INVOKED = 8109, INVALID_BANANA_IN_BOX = 8101, + ISOLATED_SHADOW_DOM_INVALID_CONTENT_PROJECTION = 2028, LET_USED_BEFORE_DEFINITION = 8016, LOCAL_COMPILATION_UNRESOLVED_CONST = 11001, LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION = 11003, diff --git a/goldens/public-api/platform-browser/errors.api.md b/goldens/public-api/platform-browser/errors.api.md index ead51d7ea4fe..c66f88b4d0c2 100644 --- a/goldens/public-api/platform-browser/errors.api.md +++ b/goldens/public-api/platform-browser/errors.api.md @@ -23,6 +23,8 @@ export const enum RuntimeErrorCode { // (undocumented) SANITIZATION_UNSAFE_SCRIPT = 5200, // (undocumented) + SHADOWDOM_NOT_SUPPORTED_IN_SSR = 5106, + // (undocumented) TESTABILITY_NOT_FOUND = 5103, // (undocumented) UNEXPECTED_SYNTHETIC_PROPERTY = 5105, diff --git a/integration/cli-hello-world-lazy/size.json b/integration/cli-hello-world-lazy/size.json index 17ca95e3d746..408cd5397df4 100644 --- a/integration/cli-hello-world-lazy/size.json +++ b/integration/cli-hello-world-lazy/size.json @@ -1,5 +1,5 @@ { - "dist/main.js": 108611, - "dist/polyfills.js": 34169, - "dist/lazy.routes-[hash].js": 361 + "dist/main.js": 113764, + "dist/polyfills.js": 34585, + "dist/lazy.routes-[hash].js": 348 } diff --git a/integration/defer/size.json b/integration/defer/size.json index 1beafbde0f15..36b4c3fc095b 100644 --- a/integration/defer/size.json +++ b/integration/defer/size.json @@ -1,5 +1,5 @@ { - "dist/main.js": 12709, + "dist/main.js": 16485, "dist/polyfills.js": 35677, - "dist/defer.component-[hash].js": 345 + "dist/defer.component-[hash].js": 331 } diff --git a/integration/legacy-animations-async/size.json b/integration/legacy-animations-async/size.json index 733d7a561d1b..95b95ced06bf 100644 --- a/integration/legacy-animations-async/size.json +++ b/integration/legacy-animations-async/size.json @@ -1,6 +1,6 @@ { - "dist/main.js": 97227, + "dist/main.js": 102215, "dist/polyfills.js": 35677, - "dist/browser-[hash].js": 63949, - "dist/open-close.component-[hash].js": 1218 + "dist/browser-[hash].js": 64236, + "dist/open-close.component-[hash].js": 1189 } diff --git a/integration/legacy-animations/size.json b/integration/legacy-animations/size.json index b578b2631982..72e4ed36f432 100644 --- a/integration/legacy-animations/size.json +++ b/integration/legacy-animations/size.json @@ -1,5 +1,5 @@ { - "dist/main.js": 153915, - "dist/polyfills.js": 34023, - "dist/open-close.component-[hash].js": 1190 + "dist/main.js": 159053, + "dist/polyfills.js": 35677, + "dist/open-close.component-[hash].js": 1131 } diff --git a/integration/platform-server-hydration/size.json b/integration/platform-server-hydration/size.json index 9464fd57a240..c7ce59822276 100644 --- a/integration/platform-server-hydration/size.json +++ b/integration/platform-server-hydration/size.json @@ -1,5 +1,5 @@ { - "dist/browser/main-[hash].js": 227093, - "dist/browser/polyfills-[hash].js": 34544, + "dist/browser/main-[hash].js": 234529, + "dist/browser/polyfills-[hash].js": 35677, "dist/browser/event-dispatch-contract.min.js": 476 } diff --git a/integration/standalone-bootstrap/size.json b/integration/standalone-bootstrap/size.json index 1a57769eb1df..e3748914ee52 100644 --- a/integration/standalone-bootstrap/size.json +++ b/integration/standalone-bootstrap/size.json @@ -1,4 +1,4 @@ { - "dist/main.js": 89237, + "dist/main.js": 97201, "dist/polyfills.js": 35677 } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index b2fcc66045c5..e368d5175bff 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -908,6 +908,24 @@ export class ComponentDecoratorHandler } } + // Check for ng-content in IsolatedShadowDom components + if (encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom) { + const contentNode = findContentNode(template.nodes); + if (contentNode !== null) { + if (diagnostics === undefined) { + diagnostics = []; + } + diagnostics.push( + makeDiagnostic( + ErrorCode.ISOLATED_SHADOW_DOM_INVALID_CONTENT_PROJECTION, + component.get('template') ?? node.name, + `ng-content projection is not supported with ViewEncapsulation.ExperimentalIsolatedShadowDom. ` + + `Use native elements instead. Content will remain in the light DOM and be projected via slots.`, + ), + ); + } + } + // If inline styles were preprocessed use those let inlineStyles: string[] | null = null; if (this.preanalyzeStylesCache.has(node)) { @@ -2637,3 +2655,24 @@ function validateStandaloneImports( function isDefaultImport(node: ts.ImportDeclaration): boolean { return node.importClause !== undefined && node.importClause.namedBindings === undefined; } + +/** + * Recursively searches through template nodes to find a Content node (ng-content). + * Returns the first Content node found, or null if none exist. + */ +function findContentNode(nodes: any[]): any | null { + for (const node of nodes) { + // Check if this is a Content node (ng-content) + if (node.name === 'ng-content') { + return node; + } + // Recursively check children + if (node.children && node.children.length > 0) { + const found = findContentNode(node.children); + if (found !== null) { + return found; + } + } + } + return null; +} diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts index c2e689d9fa9e..21acb134c7b0 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts @@ -184,6 +184,12 @@ export enum ErrorCode { */ COMPONENT_ANIMATIONS_CONFLICT = 2027, + /** + * Raised when a component with `ViewEncapsulation.ExperimentalIsolatedShadowDom` uses ``. + * ExperimentalIsolatedShadowDom components must use native `` elements instead. + */ + ISOLATED_SHADOW_DOM_INVALID_CONTENT_PROJECTION = 2028, + SYMBOL_NOT_EXPORTED = 3001, /** * Raised when a relationship between directives and/or pipes would cause a cyclic import to be diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index a0484eb66ed8..1833ac08e116 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -105,6 +105,9 @@ export {createComponent, reflectComponentType, ComponentMirror} from './render3/ export {isStandalone} from './render3/def_getters'; export {AfterRenderRef} from './render3/after_render/api'; export {publishExternalGlobalUtil as ɵpublishExternalGlobalUtil} from './render3/util/global_utils'; +export {readPatchedData as eReadPatchedData} from './render3/context_discovery'; +export {unwrapRNode as ɵunwrapRNode} from './render3/util/view_utils'; +export {HOST as ɵHOST, PARENT as ɵPARENT} from './render3/interfaces/view'; export {enableProfiling} from './render3/debug/chrome_dev_tools_performance'; export { AfterRenderOptions, @@ -126,6 +129,7 @@ export { } from './animation/interfaces'; import {global} from './util/global'; + if (typeof ngDevMode !== 'undefined' && ngDevMode) { // This helper is to give a reasonable error message to people upgrading to v9 that have not yet // installed `@angular/localize` in their app. diff --git a/packages/core/src/render3/instructions/projection.ts b/packages/core/src/render3/instructions/projection.ts index 7e2630c05ec4..e52a9b002cee 100644 --- a/packages/core/src/render3/instructions/projection.ts +++ b/packages/core/src/render3/instructions/projection.ts @@ -11,6 +11,7 @@ import {newArray} from '../../util/array_utils'; import {assertLContainer, assertTNode} from '../assert'; import {ComponentTemplate} from '../interfaces/definition'; import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node'; +import {ViewEncapsulation} from '../../metadata/view'; import {ProjectionSlots} from '../interfaces/projection'; import { DECLARATION_COMPONENT_VIEW, @@ -32,6 +33,7 @@ import {addLViewToLContainer} from '../view/container'; import {createAndRenderEmbeddedLView, shouldAddViewToDom} from '../view_manipulation'; import {declareNoDirectiveHostTemplate} from './template'; +import {getDeclarationComponentDef} from '../../render3/instructions/element_validation'; /** * Checks a given node against matching projection slots and returns the @@ -94,7 +96,24 @@ export function matchingProjectionSlotIndex( * @codeGenApi */ export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void { - const componentNode = getLView()[DECLARATION_COMPONENT_VIEW][T_HOST] as TElementNode; + const lView = getLView(); + const declarationComponentView = lView[DECLARATION_COMPONENT_VIEW]; + const componentNode = declarationComponentView[T_HOST] as TElementNode; + + // Check if this is an IsolatedShadowDom component + // The component instance is stored in CONTEXT + const componentDef = getDeclarationComponentDef(declarationComponentView); + + if (componentDef?.encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom) { + if (typeof ngDevMode !== 'undefined' && ngDevMode) { + throw new Error( + `ng-content projection is not supported with ViewEncapsulation.IsolatedShadowDom. ` + + `Use native elements instead. Content will remain in the light DOM and be projected via slots.`, + ); + } + // Don't setup projection for IsolatedShadowDom + return; + } if (!componentNode.projection) { // If no explicit projection slots are defined, fall back to a single diff --git a/packages/core/test/acceptance/renderer_factory_spec.ts b/packages/core/test/acceptance/renderer_factory_spec.ts index 26df946073e9..7c9211e784eb 100644 --- a/packages/core/test/acceptance/renderer_factory_spec.ts +++ b/packages/core/test/acceptance/renderer_factory_spec.ts @@ -23,6 +23,7 @@ import { ɵDomRendererFactory2 as DomRendererFactory2, EventManager, ɵSharedStylesHost, + ɵStyleScopeService as StyleScopeService, } from '@angular/platform-browser'; import {isBrowser, isNode} from '@angular/private/testing'; import {expect} from '@angular/private/testing/matchers'; @@ -396,6 +397,7 @@ function getRendererFactory2(document: Document): RendererFactory2 { const fakeNgZone: NgZone = new NoopNgZone(); const eventManager = new EventManager([], fakeNgZone); const appId = 'app-id'; + const styleScopeService = new StyleScopeService(); const rendererFactory = new DomRendererFactory2( eventManager, new ɵSharedStylesHost(document, appId), @@ -404,6 +406,8 @@ function getRendererFactory2(document: Document): RendererFactory2 { document, fakeNgZone, null, + null, // tracingService + styleScopeService, ); const origCreateRenderer = rendererFactory.createRenderer; rendererFactory.createRenderer = function (element: any, type: RendererType2 | null) { diff --git a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json index 951f301d8a97..10d1a8c1dd4b 100644 --- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json @@ -132,6 +132,7 @@ "InjectionToken", "Injector", "InputFlags", + "IsolatedStyleScopeService", "KeyEventsPlugin", "LEAVE_CLASSNAME", "LEAVE_TOKEN", @@ -508,6 +509,7 @@ "getInsertInFrontOfRNode", "getInsertInFrontOfRNodeWithNoI18n", "getLView", + "getLViewById", "getLViewParent", "getNameOnlyMarkerIndex", "getNamespace", @@ -742,6 +744,7 @@ "providerToRecord", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "refreshContentQueries", "refreshView", "registerFailed", diff --git a/packages/core/test/bundling/create_component/bundle.golden_symbols.json b/packages/core/test/bundling/create_component/bundle.golden_symbols.json index 20e5657ff58a..f4eaf399e782 100644 --- a/packages/core/test/bundling/create_component/bundle.golden_symbols.json +++ b/packages/core/test/bundling/create_component/bundle.golden_symbols.json @@ -98,6 +98,7 @@ "InjectionToken", "Injector", "InputFlags", + "IsolatedStyleScopeService", "KeyEventsPlugin", "LOCALE_ID", "LOCALE_ID", @@ -406,6 +407,7 @@ "getInsertInFrontOfRNode", "getInsertInFrontOfRNodeWithNoI18n", "getLView", + "getLViewById", "getLViewParent", "getNativeByIndex", "getNativeByTNode", @@ -588,6 +590,7 @@ "providerToRecord", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "refreshContentQueries", "refreshView", "registerHostBindingOpCodes", diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index 900b22feb46a..0be858986093 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -24,6 +24,7 @@ "EventManagerPlugin", "HOST_ATTR", "INTERNAL_BROWSER_PLATFORM_PROVIDERS", + "IsolatedStyleScopeService", "KeyEventsPlugin", "MODIFIER_KEYS", "MODIFIER_KEY_GETTERS", @@ -456,6 +457,7 @@ "getInsertInFrontOfRNodeWithNoI18n", "getLDeferBlockDetails", "getLView", + "getLViewById", "getLViewParent", "getLoadingBlockAfter", "getMinimumDurationForState", @@ -644,6 +646,7 @@ "providerToRecord", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "refreshContentQueries", "refreshView", "registerHostBindingOpCodes", diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index 9313004bfd38..351421a43a6a 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -133,6 +133,7 @@ "InjectionToken", "Injector", "InputFlags", + "IsolatedStyleScopeService", "IterableChangeRecord_", "IterableDiffers", "KeyEventsPlugin", @@ -574,6 +575,7 @@ "getInsertInFrontOfRNode", "getInsertInFrontOfRNodeWithNoI18n", "getLView", + "getLViewById", "getLViewParent", "getNameOnlyMarkerIndex", "getNamespace", @@ -859,6 +861,7 @@ "providersResolver", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "readableStreamLikeToAsyncGenerator", "refreshContentQueries", "refreshView", diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index c7814601060e..a7a334da9ceb 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -127,6 +127,7 @@ "InjectionToken", "Injector", "InputFlags", + "IsolatedStyleScopeService", "IterableChangeRecord_", "IterableDiffers", "KeyEventsPlugin", @@ -575,6 +576,7 @@ "getInsertInFrontOfRNode", "getInsertInFrontOfRNodeWithNoI18n", "getLView", + "getLViewById", "getLViewParent", "getNameOnlyMarkerIndex", "getNamespace", @@ -857,6 +859,7 @@ "providersResolver", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "readableStreamLikeToAsyncGenerator", "refreshContentQueries", "refreshView", diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index df18ccd711c6..65fb98d40acc 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -115,6 +115,7 @@ "InjectionToken", "Injector", "InputFlags", + "IsolatedStyleScopeService", "JSON_CONTENT_TYPE", "KeyEventsPlugin", "LOCALE_ID", @@ -477,6 +478,7 @@ "getInsertInFrontOfRNodeWithNoI18n", "getLNodeForHydration", "getLView", + "getLViewById", "getLViewParent", "getNamespace", "getNativeByTNode", @@ -683,6 +685,7 @@ "providerToRecord", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "readableStreamLikeToAsyncGenerator", "refreshContentQueries", "refreshView", diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 965f6e60df2f..23cc0e8a1796 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -131,6 +131,7 @@ "InjectionToken", "Injector", "InputFlags", + "IsolatedStyleScopeService", "ItemComponent", "KeyEventsPlugin", "LOCALE_ID", @@ -665,6 +666,7 @@ "getInsertInFrontOfRNode", "getInsertInFrontOfRNodeWithNoI18n", "getLView", + "getLViewById", "getLViewParent", "getNameOnlyMarkerIndex", "getNamespace", @@ -967,6 +969,7 @@ "providerToRecord", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "readableStreamLikeToAsyncGenerator", "recognize", "recognize", diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json index e9f7ff4db879..c24772d68287 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -94,6 +94,7 @@ "InjectionToken", "Injector", "InputFlags", + "IsolatedStyleScopeService", "KeyEventsPlugin", "LOCALE_ID", "LOCALE_ID", @@ -378,6 +379,7 @@ "getInsertInFrontOfRNode", "getInsertInFrontOfRNodeWithNoI18n", "getLView", + "getLViewById", "getLViewParent", "getNativeByTNode", "getNearestLContainer", @@ -539,6 +541,7 @@ "providerToRecord", "publishSignalConfiguration", "queueEnterAnimations", + "readPatchedData", "refreshContentQueries", "refreshView", "registerHostBindingOpCodes", diff --git a/packages/core/test/render3/imported_renderer2.ts b/packages/core/test/render3/imported_renderer2.ts index 2b86d8fa4b6f..8304e9262a35 100644 --- a/packages/core/test/render3/imported_renderer2.ts +++ b/packages/core/test/render3/imported_renderer2.ts @@ -15,6 +15,7 @@ import { EventManagerPlugin, ɵDomRendererFactory2, ɵSharedStylesHost, + ɵStyleScopeService, } from '@angular/platform-browser'; import {isNode} from '@angular/private/testing'; import {type ListenerOptions, NgZone, RendererFactory2, RendererType2} from '../../src/core'; @@ -54,6 +55,7 @@ export function getRendererFactory2(document: any): RendererFactory2 { const fakeNgZone: NgZone = new NoopNgZone(); const eventManager = new EventManager([new SimpleDomEventsPlugin(document)], fakeNgZone); const appId = 'appid'; + const styleScopeService = new ɵStyleScopeService(); const rendererFactory = new ɵDomRendererFactory2( eventManager, new ɵSharedStylesHost(document, appId), @@ -62,6 +64,8 @@ export function getRendererFactory2(document: any): RendererFactory2 { document, fakeNgZone, null, + null, // tracingService + styleScopeService, ); const origCreateRenderer = rendererFactory.createRenderer; rendererFactory.createRenderer = function (element: any, type: RendererType2 | null) { diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts index d983439ecacc..14e758d879e9 100644 --- a/packages/platform-browser/src/dom/dom_renderer.ts +++ b/packages/platform-browser/src/dom/dom_renderer.ts @@ -32,6 +32,7 @@ import {RuntimeErrorCode} from '../errors'; import {EventManager} from './events/event_manager'; import {createLinkElement, SharedStylesHost} from './shared_styles_host'; +import {IsolatedStyleScopeService} from './isolated_style_scope'; export const NAMESPACE_URIS: {[ns: string]: string} = { 'svg': 'http://www.w3.org/2000/svg', @@ -147,6 +148,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { @Inject(TracingService) @Optional() private readonly tracingService: TracingService | null = null, + private readonly styleScopeService: IsolatedStyleScopeService, ) { this.platformIsServer = typeof ngServerMode !== 'undefined' && ngServerMode; this.defaultRenderer = new DefaultDomRenderer2( @@ -179,13 +181,13 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { if (renderer instanceof EmulatedEncapsulationDomRenderer2) { renderer.applyToHost(element); } else if (renderer instanceof NoneEncapsulationDomRenderer) { - renderer.applyStyles(); + renderer.applyStyles(element); } return renderer; } - private getOrCreateRenderer(element: any, type: RendererType2): Renderer2 { + private getOrCreateRenderer(element: HTMLElement, type: RendererType2): Renderer2 { const rendererByCompId = this.rendererByCompId; let renderer = rendererByCompId.get(type.id); @@ -210,20 +212,10 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { ngZone, platformIsServer, tracingService, + this.styleScopeService, ); break; case ViewEncapsulation.ShadowDom: - return new ShadowDomRenderer( - eventManager, - element, - type, - doc, - ngZone, - this.nonce, - platformIsServer, - tracingService, - sharedStylesHost, - ); case ViewEncapsulation.ExperimentalIsolatedShadowDom: return new ShadowDomRenderer( eventManager, @@ -234,6 +226,8 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { this.nonce, platformIsServer, tracingService, + sharedStylesHost, + this.styleScopeService, ); default: @@ -246,6 +240,8 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { ngZone, platformIsServer, tracingService, + this.styleScopeService, + undefined, ); break; } @@ -282,7 +278,7 @@ class DefaultDomRenderer2 implements Renderer2 { private readonly eventManager: EventManager, private readonly doc: Document, protected readonly ngZone: NgZone, - private readonly platformIsServer: boolean, + protected readonly platformIsServer: boolean, private readonly tracingService: TracingService | null, ) {} @@ -505,27 +501,48 @@ function isTemplateNode(node: any): node is HTMLTemplateElement { } class ShadowDomRenderer extends DefaultDomRenderer2 { - private shadowRoot: any; + private shadowRoot: ShadowRoot; constructor( eventManager: EventManager, - private hostEl: any, - component: RendererType2, + private hostEl: HTMLElement, + private component: RendererType2, doc: Document, ngZone: NgZone, nonce: string | null, platformIsServer: boolean, tracingService: TracingService | null, - private sharedStylesHost?: SharedStylesHost, + private sharedStylesHost: SharedStylesHost, + private styleScopeService: IsolatedStyleScopeService, ) { super(eventManager, doc, ngZone, platformIsServer, tracingService); - this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'}); - // SharedStylesHost is used to add styles to the shadow root by ShadowDom. - // This is optional as it is not used by ExperimentalIsolatedShadowDom. - if (this.sharedStylesHost) { + const isIsolated = component.encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom; + + // Only create shadow root in browser environments + if (!platformIsServer) { + this.shadowRoot = hostEl.attachShadow({mode: 'open'}); + } else { + // In SSR or environments without shadow DOM support, throw + throw new RuntimeError( + RuntimeErrorCode.SHADOWDOM_NOT_SUPPORTED_IN_SSR, + typeof ngDevMode !== 'undefined' && ngDevMode + ? 'Shadowdom is not supported in SSR mode until declarative shadow DOM is supported.' + : '', + ); + } + + // Determine if this is isolated based on component encapsulation + + // Register shadow root with StyleScopeService for style targeting (only if service available and in browser) + if (isIsolated) { + this.styleScopeService.registerIsolatedShadowRoot(this.shadowRoot); + this.sharedStylesHost.addShadowRoot(this.shadowRoot); + } else { + this.styleScopeService.registerStandardShadowRoot(this.shadowRoot); this.sharedStylesHost.addHost(this.shadowRoot); } + let styles = component.styles; if (ngDevMode) { // We only do this in development, as for production users should not add CSS sourcemaps to components. @@ -533,50 +550,56 @@ class ShadowDomRenderer extends DefaultDomRenderer2 { styles = addBaseHrefToCssSourceMap(baseHref, styles); } - styles = shimStylesContent(component.id, styles); + if (isIsolated) { + // For IsolatedShadowDom, use SharedStylesHost with shadowRoot targeting + this.sharedStylesHost.addStyles(styles, component.getExternalStyles?.(), this.shadowRoot); + } else { + // For standard ShadowDom or SSR, use original approach + styles = shimStylesContent(component.id, styles); - for (const style of styles) { - const styleEl = document.createElement('style'); + for (const style of styles) { + const styleEl = doc.createElement('style'); - if (nonce) { - styleEl.setAttribute('nonce', nonce); - } + if (nonce) { + styleEl.setAttribute('nonce', nonce); + } - styleEl.textContent = style; - this.shadowRoot.appendChild(styleEl); - } + styleEl.textContent = style; + this.shadowRoot.appendChild(styleEl); + } - // Apply any external component styles to the shadow root for the component's element. - // The ShadowDOM renderer uses an alternative execution path for component styles that - // does not use the SharedStylesHost that other encapsulation modes leverage. Much like - // the manual addition of embedded styles directly above, any external stylesheets - // must be manually added here to ensure ShadowDOM components are correctly styled. - // TODO: Consider reworking the DOM Renderers to consolidate style handling. - const styleUrls = component.getExternalStyles?.(); - if (styleUrls) { - for (const styleUrl of styleUrls) { - const linkEl = createLinkElement(styleUrl, doc); - if (nonce) { - linkEl.setAttribute('nonce', nonce); + // Apply any external component styles to the shadow root for the component's element. + // The ShadowDOM renderer uses an alternative execution path for component styles that + // does not use the SharedStylesHost that other encapsulation modes leverage. Much like + // the manual addition of embedded styles directly above, any external stylesheets + // must be manually added here to ensure ShadowDOM components are correctly styled. + // TODO: Consider reworking the DOM Renderers to consolidate style handling. + const styleUrls = component.getExternalStyles?.(); + if (styleUrls) { + for (const styleUrl of styleUrls) { + const linkEl = createLinkElement(styleUrl, doc); + if (nonce) { + linkEl.setAttribute('nonce', nonce); + } + this.shadowRoot.appendChild(linkEl); } - this.shadowRoot.appendChild(linkEl); } } } - private nodeOrShadowRoot(node: any): any { + private nodeOrShadowRoot(node: HTMLElement): HTMLElement | ShadowRoot { return node === this.hostEl ? this.shadowRoot : node; } - override appendChild(parent: any, newChild: any): void { + override appendChild(parent: HTMLElement, newChild: Node): void { return super.appendChild(this.nodeOrShadowRoot(parent), newChild); } - override insertBefore(parent: any, newChild: any, refChild: any): void { + override insertBefore(parent: HTMLElement, newChild: Node, refChild: Node): void { return super.insertBefore(this.nodeOrShadowRoot(parent), newChild, refChild); } - override removeChild(_parent: any, oldChild: any): void { + override removeChild(_parent: HTMLElement, oldChild: Node): void { return super.removeChild(null, oldChild); } @@ -585,25 +608,31 @@ class ShadowDomRenderer extends DefaultDomRenderer2 { } override destroy() { - if (this.sharedStylesHost) { + // Query the service to determine what type this shadow root is (only if service available and in browser) + if (this.styleScopeService.isIsolatedShadowRoot(this.shadowRoot)) { + this.styleScopeService.deregisterIsolatedShadowRoot(this.shadowRoot); + this.sharedStylesHost.removeShadowRoot(this.shadowRoot); + } else if (this.styleScopeService.isStandardShadowRoot(this.shadowRoot)) { + this.styleScopeService.deregisterStandardShadowRoot(this.shadowRoot); this.sharedStylesHost.removeHost(this.shadowRoot); } } } class NoneEncapsulationDomRenderer extends DefaultDomRenderer2 { - private readonly styles: string[]; - private readonly styleUrls?: string[]; + protected readonly styles: string[]; + protected readonly styleUrls?: string[]; constructor( eventManager: EventManager, - private readonly sharedStylesHost: SharedStylesHost, + protected readonly sharedStylesHost: SharedStylesHost, component: RendererType2, private removeStylesOnCompDestroy: boolean, doc: Document, ngZone: NgZone, platformIsServer: boolean, tracingService: TracingService | null, + protected styleScopeService: IsolatedStyleScopeService, compId?: string, ) { super(eventManager, doc, ngZone, platformIsServer, tracingService); @@ -618,7 +647,19 @@ class NoneEncapsulationDomRenderer extends DefaultDomRenderer2 { this.styleUrls = component.getExternalStyles?.(compId); } - applyStyles(): void { + applyStyles(element: HTMLElement): void { + // Check if we should target specific shadow roots (only if service and element available) + if (element && !this.platformIsServer) { + const targetShadowRoots = this.styleScopeService.determineStyleTargets(element); + if (targetShadowRoots.length > 0) { + for (const shadowRoot of targetShadowRoots) { + this.sharedStylesHost.addStyles(this.styles, this.styleUrls, shadowRoot); + } + return; + } + } + + // Default behavior: apply to document head and all standard shadow hosts this.sharedStylesHost.addStyles(this.styles, this.styleUrls); } @@ -646,6 +687,7 @@ class EmulatedEncapsulationDomRenderer2 extends NoneEncapsulationDomRenderer { ngZone: NgZone, platformIsServer: boolean, tracingService: TracingService | null, + styleScopeService: IsolatedStyleScopeService, ) { const compId = appId + '-' + component.id; super( @@ -657,14 +699,16 @@ class EmulatedEncapsulationDomRenderer2 extends NoneEncapsulationDomRenderer { ngZone, platformIsServer, tracingService, + styleScopeService, compId, ); this.contentAttr = shimContentAttribute(compId); this.hostAttr = shimHostAttribute(compId); } - applyToHost(element: any): void { - this.applyStyles(); + applyToHost(element: HTMLElement): void { + // Use inherited applyStyles method to handle shadow root targeting and fallback + this.applyStyles(element); this.setAttribute(element, this.hostAttr, ''); } diff --git a/packages/platform-browser/src/dom/isolated_style_scope.ts b/packages/platform-browser/src/dom/isolated_style_scope.ts new file mode 100644 index 000000000000..72a2b6aab182 --- /dev/null +++ b/packages/platform-browser/src/dom/isolated_style_scope.ts @@ -0,0 +1,154 @@ +/** + * @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 { + Injectable, + ɵunwrapRNode as unwrapRNode, + eReadPatchedData as readPatchedData, + ɵHOST as HOST, + ɵPARENT as PARENT, +} from '@angular/core'; + +/** + * Service that tracks active shadow DOM contexts and determines where styles should be applied + * for IsolatedShadowDom encapsulation. + */ +@Injectable({providedIn: 'root'}) +export class IsolatedStyleScopeService { + // Track isolated shadow roots with their host elements + private isolatedShadowRoots = new Map(); + // Track standard shadow roots with their host elements + private standardShadowRoots = new Map(); + + registerIsolatedShadowRoot(shadowRoot: ShadowRoot): void { + this.isolatedShadowRoots.set(shadowRoot, shadowRoot.host); + } + + registerStandardShadowRoot(shadowRoot: ShadowRoot): void { + this.standardShadowRoots.set(shadowRoot, shadowRoot.host); + } + + deregisterIsolatedShadowRoot(shadowRoot: ShadowRoot): void { + this.isolatedShadowRoots.delete(shadowRoot); + } + + deregisterStandardShadowRoot(shadowRoot: ShadowRoot): void { + this.standardShadowRoots.delete(shadowRoot); + } + + /** + * Determines where styles should be applied by checking the shadow DOM context. + * Uses Angular's LView hierarchy combined with DOM checks for robustness. + */ + determineStyleTargets(element: HTMLElement): ShadowRoot[] { + if (typeof ShadowRoot === 'undefined') { + return []; + } + + // Check if element is already inside a shadow root + const elementRoot = element.getRootNode(); + if (elementRoot instanceof ShadowRoot && this.isRegisteredShadowRoot(elementRoot)) { + return this.getShadowRootsForContext(elementRoot); + } + + // Try Angular's LView hierarchy with DOM ancestor checking + try { + const result = this.findShadowRootViaLView(element); + if (result) { + return result; + } + } catch (e) { + // LView not available, return empty to use broadcast behavior + } + + return []; + } + + /** + * Finds shadow root hosts using Angular's LView hierarchy. + * + * Note: With IsolatedShadowDom, ng-content projection is disabled and native + * elements are used instead. This means projected content stays in the light DOM, + * so we only need to check the LView hierarchy - no DOM walking required. + */ + private findShadowRootViaLView(element: HTMLElement): ShadowRoot[] | null { + const lView = readPatchedData(element); + if (!lView || !Array.isArray(lView)) { + return null; + } + + // Traverse LView hierarchy to find component hosts + let currentLView = lView; + const visited = new Set(); + + while (currentLView && !visited.has(currentLView)) { + visited.add(currentLView); + + const hostRNode = currentLView[HOST]; + if (hostRNode) { + const hostElement = unwrapRNode(hostRNode); + if (hostElement instanceof Element) { + const shadowRoots = this.checkIfShadowRootHost(hostElement); + if (shadowRoots) { + return shadowRoots; + } + } + } + + const parentLView = currentLView[PARENT]; + if (parentLView && Array.isArray(parentLView) && parentLView[HOST] !== undefined) { + currentLView = parentLView as any; + } else { + break; + } + } + + return null; + } + + /** + * Checks if the given element is a shadow root host and returns appropriate shadow roots. + */ + private checkIfShadowRootHost(element: Element): ShadowRoot[] | null { + for (const [shadowRoot, host] of [...this.isolatedShadowRoots, ...this.standardShadowRoots]) { + if (host === element) { + return this.getShadowRootsForContext(shadowRoot); + } + } + return null; + } + + /** + * Returns the appropriate shadow roots based on whether it's isolated or standard. + */ + private getShadowRootsForContext(shadowRoot: ShadowRoot): ShadowRoot[] { + if (this.isIsolatedShadowRoot(shadowRoot)) { + return [shadowRoot]; + } else { + return Array.from(this.standardShadowRoots.keys()); + } + } + + private isRegisteredShadowRoot(shadowRoot: ShadowRoot): boolean { + return this.isolatedShadowRoots.has(shadowRoot) || this.standardShadowRoots.has(shadowRoot); + } + + /** + * Check if a shadow root is registered as an isolated shadow root + */ + isIsolatedShadowRoot(shadowRoot: ShadowRoot): boolean { + return this.isolatedShadowRoots.has(shadowRoot); + } + + /** + * Check if a shadow root is registered as a standard shadow root + */ + isStandardShadowRoot(shadowRoot: ShadowRoot): boolean { + return this.standardShadowRoots.has(shadowRoot); + } +} diff --git a/packages/platform-browser/src/dom/shared_styles_host.ts b/packages/platform-browser/src/dom/shared_styles_host.ts index 19101a833f20..f1d4a01cdd50 100644 --- a/packages/platform-browser/src/dom/shared_styles_host.ts +++ b/packages/platform-browser/src/dom/shared_styles_host.ts @@ -104,21 +104,28 @@ export function createLinkElement(url: string, doc: Document): HTMLLinkElement { @Injectable() export class SharedStylesHost implements OnDestroy { /** - * Provides usage information for active inline style content and associated HTML