From bef4e80e4f048c1123419b2d434fbf34c3a4ca49 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 15 May 2020 19:31:29 +0200 Subject: [PATCH] fix(icon): cancel in-flight icon requests if the icon changes (#19303) --- src/material/icon/icon.spec.ts | 36 ++++++++++++++++++++++++++++++++++ src/material/icon/icon.ts | 10 +++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/material/icon/icon.spec.ts b/src/material/icon/icon.spec.ts index 40af03bbe364..7af15fb6bf4f 100644 --- a/src/material/icon/icon.spec.ts +++ b/src/material/icon/icon.spec.ts @@ -595,6 +595,42 @@ describe('MatIcon', () => { tick(); })); + it('should cancel in-progress fetches if the icon changes', fakeAsync(() => { + // Register an icon that will resolve immediately. + iconRegistry.addSvgIconLiteral('fluffy', trustHtml(FAKE_SVGS.cat)); + + // Register a different icon that takes some time to resolve. + iconRegistry.addSvgIcon('fido', trustUrl('dog.svg')); + + const fixture = TestBed.createComponent(IconFromSvgName); + const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon'); + + // Assign the slow icon first. + fixture.componentInstance.iconName = 'fido'; + fixture.detectChanges(); + + // Assign the quick icon while the slow one is still in-flight. + fixture.componentInstance.iconName = 'fluffy'; + fixture.detectChanges(); + + // Expect for the in-flight request to have been cancelled. + expect(http.expectOne('dog.svg').cancelled).toBe(true); + + // Expect the last icon to have been assigned. + verifyPathChildElement(verifyAndGetSingleSvgChild(iconElement), 'meow'); + })); + + it('should cancel in-progress fetches if the component is destroyed', fakeAsync(() => { + iconRegistry.addSvgIcon('fido', trustUrl('dog.svg')); + + const fixture = TestBed.createComponent(IconFromSvgName); + fixture.componentInstance.iconName = 'fido'; + fixture.detectChanges(); + fixture.destroy(); + + expect(http.expectOne('dog.svg').cancelled).toBe(true); + })); + }); describe('Icons from HTML string', () => { diff --git a/src/material/icon/icon.ts b/src/material/icon/icon.ts index 0155abc74daa..06186118367e 100644 --- a/src/material/icon/icon.ts +++ b/src/material/icon/icon.ts @@ -27,6 +27,7 @@ import { ViewEncapsulation, } from '@angular/core'; import {CanColor, CanColorCtor, mixinColor} from '@angular/material/core'; +import {Subscription} from 'rxjs'; import {take} from 'rxjs/operators'; import {MatIconRegistry} from './icon-registry'; @@ -178,6 +179,9 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft /** Keeps track of the elements and attributes that we've prefixed with the current path. */ private _elementsWithExternalReferences?: Map; + /** Subscription to the current in-progress SVG icon request. */ + private _currentIconFetch = Subscription.EMPTY; + constructor( elementRef: ElementRef, private _iconRegistry: MatIconRegistry, @Attribute('aria-hidden') ariaHidden: string, @@ -227,10 +231,12 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft const svgIconChanges = changes['svgIcon']; if (svgIconChanges) { + this._currentIconFetch.unsubscribe(); + if (this.svgIcon) { const [namespace, iconName] = this._splitIconName(this.svgIcon); - this._iconRegistry.getNamedSvgIcon(iconName, namespace) + this._currentIconFetch = this._iconRegistry.getNamedSvgIcon(iconName, namespace) .pipe(take(1)) .subscribe(svg => this._setSvgElement(svg), (err: Error) => { const errorMessage = `Error retrieving icon ${namespace}:${iconName}! ${err.message}`; @@ -279,6 +285,8 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft } ngOnDestroy() { + this._currentIconFetch.unsubscribe(); + if (this._elementsWithExternalReferences) { this._elementsWithExternalReferences.clear(); }