@@ -121,6 +121,9 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
121121 */
122122 private readonly hosts = new Map < Node , number > ( ) ;
123123
124+ /** Whether this instance has been destroyed. */
125+ private destroyed = false ;
126+
124127 constructor (
125128 @Inject ( DOCUMENT ) private readonly doc : Document ,
126129 @Inject ( APP_ID ) private readonly appId : string ,
@@ -202,24 +205,53 @@ export class SharedStylesHost implements ɵSharedStylesHost, OnDestroy {
202205 removeElements ( elements ) ;
203206 }
204207 this . hosts . clear ( ) ;
208+ this . destroyed = true ;
205209 }
206210
207211 addHost ( hostNode : Node ) : void {
212+ // Adding a host after destruction will have no effect and is likely a bug in the caller.
213+ // However, some testing scenarios with fake async appear to trigger CD after `TestBed`
214+ // teardown, meaning Angular may render new components (and then immediately destroy them)
215+ // after the application is destroyed, so we have to allow this to happen and no-op.
216+ if ( this . destroyed ) return ;
217+
208218 const existingUsage = this . hosts . get ( hostNode ) ?? 0 ;
209219 if ( existingUsage === 0 ) {
220+ // // Add existing styles to new host
221+ // for (const [style, {elements}] of this.inline) {
222+ // elements.push(this.addElement(hostNode, createStyleElement(style, this.doc)));
223+ // }
224+ // for (const [url, {elements}] of this.external) {
225+ // elements.push(this.addElement(hostNode, createLinkElement(url, this.doc)));
226+ // }
227+
210228 // Add existing styles to new host
211229 for ( const [ style , { elements} ] of this . inline ) {
212- elements . push ( this . addElement ( hostNode , createStyleElement ( style , this . doc ) ) ) ;
230+ // `removeHost` currently does not actually remove styles when usage drops to zero.
231+ // Therefore removing a host to zero and then re-adding to one, could cause Angular
232+ // to duplicate the styles on the page. This check makes sure we don't add the styles
233+ // more than once.
234+ if ( ! elements . some ( ( e ) => e . parentNode === hostNode ) ) {
235+ elements . push ( this . addElement ( hostNode , createStyleElement ( style , this . doc ) ) ) ;
236+ }
213237 }
214238 for ( const [ url , { elements} ] of this . external ) {
215- elements . push ( this . addElement ( hostNode , createLinkElement ( url , this . doc ) ) ) ;
239+ if ( ! elements . some ( ( e ) => e . parentNode === hostNode ) ) {
240+ elements . push ( this . addElement ( hostNode , createLinkElement ( url , this . doc ) ) ) ;
241+ }
216242 }
217243 }
218244
219245 this . hosts . set ( hostNode , existingUsage + 1 ) ;
220246 }
221247
222248 removeHost ( hostNode : Node ) : void {
249+ // In some scenarios (such as an explicit `ApplicationRef.prototype.destroy` call),
250+ // this instance's `ngOnDestroy` method may be called before a component is destroyed and
251+ // attempts to remove its own styles. In this case, we need to no-op to avoid throwing an
252+ // error.
253+ if ( this . destroyed ) return ;
254+
223255 const usage = this . hosts . get ( hostNode ) ;
224256 if ( typeof ngDevMode !== 'undefined' && ngDevMode && usage === undefined ) {
225257 throw new Error ( 'Attempted to remove a host which was not added.' ) ;
0 commit comments