Skip to content

Commit 65786b2

Browse files
mattlewis92atscott
authored andcommitted
fix(platform-browser): prevent duplicate stylesheets from being created (angular#52019)
Fixes a bug with REMOVE_STYLES_ON_COMPONENT_DESTROY when a component is destroyed and re-created, its previous stylesheets will not be re-used and instead a new stylesheet will still be created each time. PR Close angular#52019
1 parent caa8eb2 commit 65786b2

File tree

3 files changed

+54
-15
lines changed

3 files changed

+54
-15
lines changed

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const APP_ID_ATTRIBUTE_NAME = 'ng-app-id';
1616
export class SharedStylesHost implements OnDestroy {
1717
// Maps all registered host nodes to a list of style nodes that have been added to the host node.
1818
private readonly styleRef = new Map < string /** Style string */, {
19-
elements: HTMLStyleElement[];
19+
elements: Map</** Host */ Node, /** Style Node */ HTMLStyleElement>;
2020
usage: number;
2121
}
2222
> ();
@@ -84,6 +84,9 @@ export class SharedStylesHost implements OnDestroy {
8484

8585
removeHost(hostNode: Node): void {
8686
this.hostNodes.delete(hostNode);
87+
for (const {elements} of this.styleRef.values()) {
88+
elements.delete(hostNode);
89+
}
8790
}
8891

8992
private getAllStyles(): IterableIterator<string> {
@@ -125,11 +128,17 @@ export class SharedStylesHost implements OnDestroy {
125128
}
126129

127130
const usage = nonNegativeNumber(delta);
128-
map.set(style, {usage, elements: []});
131+
map.set(style, {usage, elements: new Map()});
129132
return usage;
130133
}
131134

132-
private getStyleElement(host: Node, style: string): HTMLStyleElement {
135+
private getStyleElement(
136+
host: Node, style: string,
137+
existingStyleElements: Map<Node, HTMLStyleElement>|undefined): HTMLStyleElement {
138+
const existingStyleElement = existingStyleElements?.get(host);
139+
if (existingStyleElement) {
140+
return existingStyleElement;
141+
}
133142
const styleNodesInDOM = this.styleNodesInDOM;
134143
const styleEl = styleNodesInDOM?.get(style);
135144
if (styleEl?.parentNode === host) {
@@ -162,23 +171,19 @@ export class SharedStylesHost implements OnDestroy {
162171
}
163172

164173
private addStyleToHost(host: Node, style: string): void {
165-
const styleEl = this.getStyleElement(host, style);
174+
const styleRef = this.styleRef;
175+
const styleResult = styleRef.get(style)!; // This will always be defined in `changeUsageCount`
176+
const styleEl = this.getStyleElement(host, style, styleResult.elements);
166177

167178
host.appendChild(styleEl);
168179

169-
const styleRef = this.styleRef;
170-
const styleResult = styleRef.get(style);
171-
if (styleResult) {
172-
if (styleResult.usage === 0) {
173-
disableStylesheet(styleEl);
174-
} else {
175-
enableStylesheet(styleEl);
176-
}
177-
178-
styleResult.elements.push(styleEl);
180+
if (styleResult.usage === 0) {
181+
disableStylesheet(styleEl);
179182
} else {
180-
styleRef.set(style, {elements: [styleEl], usage: 1});
183+
enableStylesheet(styleEl);
181184
}
185+
186+
styleResult.elements.set(host, styleEl);
182187
}
183188

184189
private resetHostNodes(): void {

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,31 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
262262
expect(styles?.length).toBe(1);
263263
expect(styles?.[0].nativeElement.disabled).toBeTrue();
264264
});
265+
266+
it('should not add duplicate stylesheets', async () => {
267+
const fixture = TestBed.createComponent(SomeAppForCleanUp);
268+
const compInstance = fixture.componentInstance;
269+
compInstance.showEmulatedComponents = false;
270+
compInstance.componentTwoInstanceHidden = true;
271+
272+
fixture.detectChanges();
273+
// verify style is in DOM
274+
expect(await styleCount(fixture, '.none')).toBe(1);
275+
276+
// Remove a single instance of the component.
277+
compInstance.componentOneInstanceHidden = true;
278+
fixture.detectChanges();
279+
280+
// Verify style is still in DOM
281+
expect(await styleCount(fixture, '.none')).toBe(1);
282+
283+
// Re-create the component
284+
compInstance.componentOneInstanceHidden = false;
285+
fixture.detectChanges();
286+
287+
// Verify there is only 1 style
288+
expect(await styleCount(fixture, '.none')).toBe(1);
289+
});
265290
});
266291
});
267292
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,14 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
6161
ssh.addHost(someHost);
6262
expect(someHost.innerHTML).toEqual('<style nonce="{% nonce %}">a {};</style>');
6363
});
64+
65+
it('should not add duplicate style nodes', () => {
66+
ssh.addHost(someHost);
67+
const styles = ['a {};'];
68+
ssh.addStyles(styles);
69+
ssh.disableStyles(styles);
70+
ssh.addStyles(styles);
71+
expect(someHost.innerHTML).toEqual('<style>a {};</style>');
72+
});
6473
});
6574
}

0 commit comments

Comments
 (0)