Skip to content

Commit 8813776

Browse files
committed
fix(platform-browser): Fixes IsolatedShadowDom encapsulation method
This fixes an issue where a child component inside of IsolatedShadowDom would get it's styles moved to the root style object, causing it to be unstyled. This new approach hoists the styles to the shadowroot. This change only applies to IsolatedShadowDom
1 parent 1846874 commit 8813776

11 files changed

Lines changed: 73 additions & 49 deletions

File tree

integration/defer/size.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"dist/main.js": 12709,
3-
"dist/polyfills.js": 33807,
4-
"dist/defer.component-[hash].js": 345
2+
"dist/main.js": 13891,
3+
"dist/polyfills.js": 34583,
4+
"dist/defer.component-[hash].js": 331
55
}

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@
200200
"PLATFORM_DESTROY_LISTENERS",
201201
"PLATFORM_ID",
202202
"PLATFORM_INITIALIZER",
203+
"PLATFORM_SERVER_ID",
203204
"PREORDER_HOOK_FLAGS",
204205
"PRESERVE_HOST_CONTENT",
205206
"PRESERVE_HOST_CONTENT_DEFAULT",
@@ -647,6 +648,7 @@
647648
"isNonAnimatableStyle",
648649
"isNotFound",
649650
"isObserver",
651+
"isPlatformServer",
650652
"isPositive",
651653
"isPromise",
652654
"isRefreshingViews",

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"NAMESPACE_URIS",
3131
"NoneEncapsulationDomRenderer",
3232
"PLATFORM_BROWSER_ID",
33+
"PLATFORM_SERVER_ID",
3334
"REMOVE_STYLES_ON_COMPONENT_DESTROY",
3435
"REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT",
3536
"ShadowDomRenderer",
@@ -48,6 +49,7 @@
4849
"getBaseElementHref",
4950
"getDOM",
5051
"initDomAdapter",
52+
"isPlatformServer",
5153
"isTemplateNode",
5254
"parseCookieValue",
5355
"relativePath",

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
"PLATFORM_DESTROY_LISTENERS",
206206
"PLATFORM_ID",
207207
"PLATFORM_INITIALIZER",
208+
"PLATFORM_SERVER_ID",
208209
"PREORDER_HOOK_FLAGS",
209210
"PRESERVE_HOST_CONTENT",
210211
"PRESERVE_HOST_CONTENT_DEFAULT",
@@ -744,6 +745,7 @@
744745
"isObserver",
745746
"isOptionsObj",
746747
"isPOJO",
748+
"isPlatformServer",
747749
"isPositive",
748750
"isPresent",
749751
"isPromise",

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
"PLATFORM_DESTROY_LISTENERS",
203203
"PLATFORM_ID",
204204
"PLATFORM_INITIALIZER",
205+
"PLATFORM_SERVER_ID",
205206
"PREORDER_HOOK_FLAGS",
206207
"PRESERVE_HOST_CONTENT",
207208
"PRESERVE_HOST_CONTENT_DEFAULT",
@@ -744,6 +745,7 @@
744745
"isObserver",
745746
"isOptionsObj",
746747
"isPOJO",
748+
"isPlatformServer",
747749
"isPositive",
748750
"isPresent",
749751
"isPromise",

packages/core/test/bundling/hydration/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@
176176
"PLATFORM_DESTROY_LISTENERS",
177177
"PLATFORM_ID",
178178
"PLATFORM_INITIALIZER",
179+
"PLATFORM_SERVER_ID",
179180
"PREORDER_HOOK_FLAGS",
180181
"PRESERVE_HOST_CONTENT",
181182
"PRESERVE_HOST_CONTENT_DEFAULT",
@@ -601,6 +602,7 @@
601602
"isLView",
602603
"isNotFound",
603604
"isObserver",
605+
"isPlatformServer",
604606
"isPositive",
605607
"isPromise",
606608
"isPromise2",

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@
214214
"PLATFORM_DESTROY_LISTENERS",
215215
"PLATFORM_ID",
216216
"PLATFORM_INITIALIZER",
217+
"PLATFORM_SERVER_ID",
217218
"PREORDER_HOOK_FLAGS",
218219
"PRESERVE_HOST_CONTENT",
219220
"PRESERVE_HOST_CONTENT_DEFAULT",
@@ -839,6 +840,7 @@
839840
"isObservable",
840841
"isObserver",
841842
"isPOJO",
843+
"isPlatformServer",
842844
"isPositive",
843845
"isPromise",
844846
"isPromise2",

packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
"PLATFORM_DESTROY_LISTENERS",
148148
"PLATFORM_ID",
149149
"PLATFORM_INITIALIZER",
150+
"PLATFORM_SERVER_ID",
150151
"PREORDER_HOOK_FLAGS",
151152
"PRESERVE_HOST_CONTENT",
152153
"PRESERVE_HOST_CONTENT_DEFAULT",
@@ -481,6 +482,7 @@
481482
"isLView",
482483
"isNotFound",
483484
"isObserver",
485+
"isPlatformServer",
484486
"isPositive",
485487
"isPromise",
486488
"isRefreshingViews",

packages/platform-browser/src/dom/dom_renderer.ts

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -227,19 +227,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
227227
);
228228
break;
229229
case ViewEncapsulation.ShadowDom:
230-
return new ShadowDomRenderer(
231-
eventManager,
232-
element,
233-
type,
234-
doc,
235-
ngZone,
236-
this.nonce,
237-
platformIsServer,
238-
tracingService,
239-
this.registry,
240-
this.maxAnimationTimeout,
241-
sharedStylesHost,
242-
);
243230
case ViewEncapsulation.IsolatedShadowDom:
244231
return new ShadowDomRenderer(
245232
eventManager,
@@ -252,6 +239,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
252239
tracingService,
253240
this.registry,
254241
this.maxAnimationTimeout,
242+
sharedStylesHost,
255243
);
256244

257245
default:
@@ -538,15 +526,15 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
538526
constructor(
539527
eventManager: EventManager,
540528
private hostEl: any,
541-
component: RendererType2,
529+
private component: RendererType2,
542530
doc: Document,
543531
ngZone: NgZone,
544532
nonce: string | null,
545533
platformIsServer: boolean,
546534
tracingService: TracingService<TracingSnapshot> | null,
547535
registry: AnimationRemovalRegistry,
548536
maxAnimationTimeout: number,
549-
private sharedStylesHost?: SharedStylesHost,
537+
private sharedStylesHost: SharedStylesHost,
550538
) {
551539
super(
552540
eventManager,
@@ -560,8 +548,9 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
560548
this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'});
561549

562550
// SharedStylesHost is used to add styles to the shadow root by ShadowDom.
563-
// This is optional as it is not used by IsolatedShadowDom.
564-
if (this.sharedStylesHost) {
551+
if (component.encapsulation === ViewEncapsulation.IsolatedShadowDom) {
552+
this.sharedStylesHost.addShadowRoot?.(this.shadowRoot);
553+
} else {
565554
this.sharedStylesHost.addHost(this.shadowRoot);
566555
}
567556
let styles = component.styles;
@@ -571,17 +560,21 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
571560
styles = addBaseHrefToCssSourceMap(baseHref, styles);
572561
}
573562

574-
styles = shimStylesContent(component.id, styles);
563+
if (component.encapsulation === ViewEncapsulation.IsolatedShadowDom) {
564+
this.sharedStylesHost.addStyles(styles, component.getExternalStyles?.());
565+
} else {
566+
styles = shimStylesContent(component.id, styles);
567+
568+
for (const style of styles) {
569+
const styleEl = document.createElement('style');
575570

576-
for (const style of styles) {
577-
const styleEl = document.createElement('style');
571+
if (nonce) {
572+
styleEl.setAttribute('nonce', nonce);
573+
}
578574

579-
if (nonce) {
580-
styleEl.setAttribute('nonce', nonce);
575+
styleEl.textContent = style;
576+
this.shadowRoot.appendChild(styleEl);
581577
}
582-
583-
styleEl.textContent = style;
584-
this.shadowRoot.appendChild(styleEl);
585578
}
586579

587580
// Apply any external component styles to the shadow root for the component's element.
@@ -624,7 +617,11 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
624617

625618
override destroy() {
626619
if (this.sharedStylesHost) {
627-
this.sharedStylesHost.removeHost(this.shadowRoot);
620+
if (this.component.encapsulation === ViewEncapsulation.IsolatedShadowDom) {
621+
this.sharedStylesHost.removeShadowRoot?.(this.shadowRoot);
622+
} else {
623+
this.sharedStylesHost.removeHost(this.shadowRoot);
624+
}
628625
}
629626
}
630627
}

packages/platform-browser/src/dom/shared_styles_host.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {DOCUMENT} from '@angular/common';
9+
import {DOCUMENT, isPlatformServer} from '@angular/common';
1010
import {
1111
APP_ID,
1212
CSP_NONCE,
@@ -114,19 +114,16 @@ export class SharedStylesHost implements OnDestroy {
114114
* External styles typically originate from the `ɵɵExternalStylesFeature` of a rendered component.
115115
*/
116116
private readonly external = new Map<string /** URL */, UsageRecord<HTMLLinkElement>>();
117-
118-
/**
119-
* Set of host DOM nodes that will have styles attached.
120-
*/
121117
private readonly hosts = new Set<Node>();
118+
private readonly shadowRoots: Node[] = [];
122119

123120
constructor(
124121
@Inject(DOCUMENT) private readonly doc: Document,
125122
@Inject(APP_ID) private readonly appId: string,
126123
@Inject(CSP_NONCE) @Optional() private readonly nonce?: string | null,
127124
// Cannot remove it due to backward compatibility
128125
// (it seems some TGP targets might be calling this constructor directly).
129-
@Inject(PLATFORM_ID) platformId: object = {},
126+
@Inject(PLATFORM_ID) private readonly platformId: object = {},
130127
) {
131128
addServerStyles(doc, appId, this.inline, this.external);
132129
this.hosts.add(doc.head);
@@ -137,11 +134,13 @@ export class SharedStylesHost implements OnDestroy {
137134
* @param styles An array of style content strings.
138135
*/
139136
addStyles(styles: string[], urls?: string[]): void {
140-
for (const value of styles) {
141-
this.addUsage(value, this.inline, createStyleElement);
137+
const host = this.shadowRoots[this.shadowRoots.length - 1];
138+
139+
for (const style of styles) {
140+
this.addUsage(style, this.inline, createStyleElement, host);
142141
}
143142

144-
urls?.forEach((value) => this.addUsage(value, this.external, createLinkElement));
143+
urls?.forEach((url) => this.addUsage(url, this.external, createLinkElement, host));
145144
}
146145

147146
/**
@@ -160,6 +159,7 @@ export class SharedStylesHost implements OnDestroy {
160159
value: string,
161160
usages: Map<string, UsageRecord<T>>,
162161
creator: (value: string, doc: Document) => T,
162+
host?: Node,
163163
): void {
164164
// Attempt to get any current usage of the value
165165
const record = usages.get(value);
@@ -173,10 +173,14 @@ export class SharedStylesHost implements OnDestroy {
173173
}
174174
record.usage++;
175175
} else {
176+
const hosts = host ? [host] : this.hosts ? [...this.hosts] : [];
177+
if (hosts.length === 0) {
178+
return;
179+
}
176180
// Otherwise, create an entry to track the elements and add element for each host
177181
usages.set(value, {
178182
usage: 1,
179-
elements: [...this.hosts].map((host) => this.addElement(host, creator(value, this.doc))),
183+
elements: hosts.map((hostNode) => this.addElement(hostNode, creator(value, this.doc))),
180184
});
181185
}
182186
}
@@ -229,17 +233,26 @@ export class SharedStylesHost implements OnDestroy {
229233
}
230234

231235
private addElement<T extends HTMLElement>(host: Node, element: T): T {
232-
// Add a nonce if present
233236
if (this.nonce) {
234237
element.setAttribute('nonce', this.nonce);
235238
}
236-
237-
// Add application identifier when on the server to support client-side reuse
238-
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
239+
// The `ng-app-id` attribute is used on the server to identify which styles were
240+
// created by the server renderer.
241+
if (isPlatformServer(this.platformId) && host === this.doc.head) {
239242
element.setAttribute(APP_ID_ATTRIBUTE_NAME, this.appId);
240243
}
244+
host.appendChild(element);
245+
return element;
246+
}
241247

242-
// Insert the element into the DOM with the host node as parent
243-
return host.appendChild(element);
248+
addShadowRoot(shadowRoot: Node): void {
249+
this.shadowRoots.push(shadowRoot);
250+
}
251+
252+
removeShadowRoot(shadowRoot: Node): void {
253+
const index = this.shadowRoots.indexOf(shadowRoot);
254+
if (index > -1) {
255+
this.shadowRoots.splice(index, 1);
256+
}
244257
}
245258
}

0 commit comments

Comments
 (0)