Skip to content

Commit 3b5f324

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 85994fb commit 3b5f324

4 files changed

Lines changed: 55 additions & 39 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": 12993,
3+
"dist/polyfills.js": 34585,
4+
"dist/defer.component-[hash].js": 331
55
}

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

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -216,17 +216,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
216216
);
217217
break;
218218
case ViewEncapsulation.ShadowDom:
219-
return new ShadowDomRenderer(
220-
eventManager,
221-
element,
222-
type,
223-
doc,
224-
ngZone,
225-
this.nonce,
226-
platformIsServer,
227-
tracingService,
228-
sharedStylesHost,
229-
);
230219
case ViewEncapsulation.IsolatedShadowDom:
231220
return new ShadowDomRenderer(
232221
eventManager,
@@ -237,6 +226,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
237226
this.nonce,
238227
platformIsServer,
239228
tracingService,
229+
sharedStylesHost,
240230
);
241231

242232
default:
@@ -513,20 +503,21 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
513503
constructor(
514504
eventManager: EventManager,
515505
private hostEl: any,
516-
component: RendererType2,
506+
private component: RendererType2,
517507
doc: Document,
518508
ngZone: NgZone,
519509
nonce: string | null,
520510
platformIsServer: boolean,
521511
tracingService: TracingService<TracingSnapshot> | null,
522-
private sharedStylesHost?: SharedStylesHost,
512+
private sharedStylesHost: SharedStylesHost,
523513
) {
524514
super(eventManager, doc, ngZone, platformIsServer, tracingService);
525515
this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'});
526516

527517
// SharedStylesHost is used to add styles to the shadow root by ShadowDom.
528-
// This is optional as it is not used by IsolatedShadowDom.
529-
if (this.sharedStylesHost) {
518+
if (component.encapsulation === ViewEncapsulation.IsolatedShadowDom) {
519+
this.sharedStylesHost.addShadowRoot?.(this.shadowRoot);
520+
} else {
530521
this.sharedStylesHost.addHost(this.shadowRoot);
531522
}
532523
let styles = component.styles;
@@ -536,17 +527,21 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
536527
styles = addBaseHrefToCssSourceMap(baseHref, styles);
537528
}
538529

539-
styles = shimStylesContent(component.id, styles);
530+
if (component.encapsulation === ViewEncapsulation.IsolatedShadowDom) {
531+
this.sharedStylesHost.addStyles(styles, component.getExternalStyles?.());
532+
} else {
533+
styles = shimStylesContent(component.id, styles);
540534

541-
for (const style of styles) {
542-
const styleEl = document.createElement('style');
535+
for (const style of styles) {
536+
const styleEl = document.createElement('style');
543537

544-
if (nonce) {
545-
styleEl.setAttribute('nonce', nonce);
546-
}
538+
if (nonce) {
539+
styleEl.setAttribute('nonce', nonce);
540+
}
547541

548-
styleEl.textContent = style;
549-
this.shadowRoot.appendChild(styleEl);
542+
styleEl.textContent = style;
543+
this.shadowRoot.appendChild(styleEl);
544+
}
550545
}
551546

552547
// Apply any external component styles to the shadow root for the component's element.
@@ -589,7 +584,11 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
589584

590585
override destroy() {
591586
if (this.sharedStylesHost) {
592-
this.sharedStylesHost.removeHost(this.shadowRoot);
587+
if (this.component.encapsulation === ViewEncapsulation.IsolatedShadowDom) {
588+
this.sharedStylesHost.removeShadowRoot?.(this.shadowRoot);
589+
} else {
590+
this.sharedStylesHost.removeHost(this.shadowRoot);
591+
}
593592
}
594593
}
595594
}

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

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class SharedStylesHost implements OnDestroy {
119119
* Set of host DOM nodes that will have styles attached.
120120
*/
121121
private readonly hosts = new Set<Node>();
122+
private readonly shadowRoots: Node[] = [];
122123

123124
constructor(
124125
@Inject(DOCUMENT) private readonly doc: Document,
@@ -137,11 +138,13 @@ export class SharedStylesHost implements OnDestroy {
137138
* @param styles An array of style content strings.
138139
*/
139140
addStyles(styles: string[], urls?: string[]): void {
140-
for (const value of styles) {
141-
this.addUsage(value, this.inline, createStyleElement);
141+
const host = this.shadowRoots[this.shadowRoots.length - 1];
142+
143+
for (const style of styles) {
144+
this.addUsage(style, this.inline, createStyleElement, host);
142145
}
143146

144-
urls?.forEach((value) => this.addUsage(value, this.external, createLinkElement));
147+
urls?.forEach((url) => this.addUsage(url, this.external, createLinkElement, host));
145148
}
146149

147150
/**
@@ -160,6 +163,7 @@ export class SharedStylesHost implements OnDestroy {
160163
value: string,
161164
usages: Map<string, UsageRecord<T>>,
162165
creator: (value: string, doc: Document) => T,
166+
host?: Node,
163167
): void {
164168
// Attempt to get any current usage of the value
165169
const record = usages.get(value);
@@ -173,10 +177,14 @@ export class SharedStylesHost implements OnDestroy {
173177
}
174178
record.usage++;
175179
} else {
180+
const hosts = host ? [host] : this.hosts ? [...this.hosts] : [];
181+
if (hosts.length === 0) {
182+
return;
183+
}
176184
// Otherwise, create an entry to track the elements and add element for each host
177185
usages.set(value, {
178186
usage: 1,
179-
elements: [...this.hosts].map((host) => this.addElement(host, creator(value, this.doc))),
187+
elements: hosts.map((hostNode) => this.addElement(hostNode, creator(value, this.doc))),
180188
});
181189
}
182190
}
@@ -229,17 +237,26 @@ export class SharedStylesHost implements OnDestroy {
229237
}
230238

231239
private addElement<T extends HTMLElement>(host: Node, element: T): T {
232-
// Add a nonce if present
233240
if (this.nonce) {
234241
element.setAttribute('nonce', this.nonce);
235242
}
236243

237244
// Add application identifier when on the server to support client-side reuse
238-
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
245+
if (typeof ngServerMode !== 'undefined' && ngServerMode && host === this.doc.head) {
239246
element.setAttribute(APP_ID_ATTRIBUTE_NAME, this.appId);
240247
}
248+
host.appendChild(element);
249+
return element;
250+
}
241251

242-
// Insert the element into the DOM with the host node as parent
243-
return host.appendChild(element);
252+
addShadowRoot(shadowRoot: Node): void {
253+
this.shadowRoots.push(shadowRoot);
254+
}
255+
256+
removeShadowRoot(shadowRoot: Node): void {
257+
const index = this.shadowRoots.indexOf(shadowRoot);
258+
if (index > -1) {
259+
this.shadowRoots.splice(index, 1);
260+
}
244261
}
245262
}

packages/platform-browser/test/dom/dom_renderer_spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,10 @@ describe('DefaultDomRendererV2', () => {
137137
expect(window.getComputedStyle(shadow).color).toEqual('rgb(255, 0, 0)');
138138

139139
const emulated = fixture.debugElement.query(By.css('.emulated')).nativeElement;
140-
expect(window.getComputedStyle(emulated).color).toEqual('rgb(255, 0, 0)');
140+
expect(window.getComputedStyle(emulated).color).toEqual('rgb(0, 0, 255)');
141141

142142
const none = fixture.debugElement.query(By.css('.none')).nativeElement;
143-
expect(window.getComputedStyle(none).color).toEqual('rgb(255, 0, 0)');
143+
expect(window.getComputedStyle(none).color).toEqual('rgb(0, 255, 0)');
144144
});
145145

146146
it('child components of shadow components should inherit browser defaults rather than their component styles', () => {
@@ -153,10 +153,10 @@ describe('DefaultDomRendererV2', () => {
153153
expect(window.getComputedStyle(shadow).backgroundColor).toEqual('rgba(0, 0, 0, 0)');
154154

155155
const emulated = fixture.debugElement.query(By.css('.emulated')).nativeElement;
156-
expect(window.getComputedStyle(emulated).backgroundColor).toEqual('rgba(0, 0, 0, 0)');
156+
expect(window.getComputedStyle(emulated).backgroundColor).toEqual('rgb(0, 0, 255)');
157157

158158
const none = fixture.debugElement.query(By.css('.none')).nativeElement;
159-
expect(window.getComputedStyle(none).backgroundColor).toEqual('rgba(0, 0, 0, 0)');
159+
expect(window.getComputedStyle(none).backgroundColor).toEqual('rgb(0, 255, 0)');
160160
});
161161

162162
it('shadow components should not be polluted by child components styles when using IsolatedShadowDom', () => {

0 commit comments

Comments
 (0)