Skip to content

Commit 9f73933

Browse files
crisbetowagnermaciel
authored andcommitted
fix(icon): not updating svg icon assigned through setter (#20509)
Moves some logic around so the icon can respond to changes in its state through plain property assignments, rather than going through Angular change detection cycle. This makes the component easier to extend. Fixes #20470. (cherry picked from commit 4263cac)
1 parent 6d805ab commit 9f73933

File tree

3 files changed

+81
-51
lines changed

3 files changed

+81
-51
lines changed

src/material/icon/icon.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import {
55
HttpTestingController,
66
TestRequest,
77
} from '@angular/common/http/testing';
8-
import {Component, ErrorHandler} from '@angular/core';
8+
import {Component, ErrorHandler, ViewChild} from '@angular/core';
99
import {MatIconModule, MAT_ICON_LOCATION} from './index';
1010
import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry';
1111
import {FAKE_SVGS} from './fake-svgs';
1212
import {wrappedErrorMessage} from '@angular/cdk/testing/private';
13+
import {MatIcon} from './icon';
1314

1415

1516
/** Returns the CSS classes assigned to an element as a sorted array. */
@@ -64,6 +65,7 @@ describe('MatIcon', () => {
6465
InlineIcon,
6566
SvgIconWithUserContent,
6667
IconWithLigatureAndSvgBinding,
68+
BlankIcon,
6769
],
6870
providers: [
6971
{
@@ -999,6 +1001,22 @@ describe('MatIcon', () => {
9991001

10001002
});
10011003

1004+
it('should handle assigning an icon through the setter', fakeAsync(() => {
1005+
iconRegistry.addSvgIconLiteral('fido', trustHtml(FAKE_SVGS.dog));
1006+
1007+
const fixture = TestBed.createComponent(BlankIcon);
1008+
fixture.detectChanges();
1009+
let svgElement: SVGElement;
1010+
const testComponent = fixture.componentInstance;
1011+
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
1012+
1013+
testComponent.icon.svgIcon = 'fido';
1014+
fixture.detectChanges();
1015+
svgElement = verifyAndGetSingleSvgChild(iconElement);
1016+
verifyPathChildElement(svgElement, 'woof');
1017+
tick();
1018+
}));
1019+
10021020
/** Marks an SVG icon url as explicitly trusted. */
10031021
function trustUrl(iconUrl: string): SafeResourceUrl {
10041022
return sanitizer.bypassSecurityTrustResourceUrl(iconUrl);
@@ -1089,3 +1107,8 @@ class SvgIconWithUserContent {
10891107
class IconWithLigatureAndSvgBinding {
10901108
iconName: string | undefined;
10911109
}
1110+
1111+
@Component({template: `<mat-icon></mat-icon>`})
1112+
class BlankIcon {
1113+
@ViewChild(MatIcon) icon: MatIcon;
1114+
}

src/material/icon/icon.ts

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ import {
1919
Inject,
2020
InjectionToken,
2121
Input,
22-
OnChanges,
2322
OnDestroy,
2423
OnInit,
25-
SimpleChanges,
2624
ViewEncapsulation,
2725
} from '@angular/core';
2826
import {CanColor, CanColorCtor, mixinColor} from '@angular/material/core';
@@ -137,8 +135,8 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
137135
encapsulation: ViewEncapsulation.None,
138136
changeDetection: ChangeDetectionStrategy.OnPush,
139137
})
140-
export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, AfterViewChecked,
141-
CanColor, OnDestroy {
138+
export class MatIcon extends _MatIconMixinBase implements OnInit, AfterViewChecked, CanColor,
139+
OnDestroy {
142140

143141
/**
144142
* Whether the icon should be inlined, automatically sizing the icon to match the font size of
@@ -154,21 +152,43 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft
154152
private _inline: boolean = false;
155153

156154
/** Name of the icon in the SVG icon set. */
157-
@Input() svgIcon: string;
155+
@Input()
156+
get svgIcon(): string { return this._svgIcon; }
157+
set svgIcon(value: string) {
158+
if (value !== this._svgIcon) {
159+
if (value) {
160+
this._updateSvgIcon(value);
161+
} else if (this._svgIcon) {
162+
this._clearSvgElement();
163+
}
164+
this._svgIcon = value;
165+
}
166+
}
167+
private _svgIcon: string;
158168

159169
/** Font set that the icon is a part of. */
160170
@Input()
161171
get fontSet(): string { return this._fontSet; }
162172
set fontSet(value: string) {
163-
this._fontSet = this._cleanupFontValue(value);
173+
const newValue = this._cleanupFontValue(value);
174+
175+
if (newValue !== this._fontSet) {
176+
this._fontSet = newValue;
177+
this._updateFontIconClasses();
178+
}
164179
}
165180
private _fontSet: string;
166181

167182
/** Name of an icon within a font set. */
168183
@Input()
169184
get fontIcon(): string { return this._fontIcon; }
170185
set fontIcon(value: string) {
171-
this._fontIcon = this._cleanupFontValue(value);
186+
const newValue = this._cleanupFontValue(value);
187+
188+
if (newValue !== this._fontIcon) {
189+
this._fontIcon = newValue;
190+
this._updateFontIconClasses();
191+
}
172192
}
173193
private _fontIcon: string;
174194

@@ -226,49 +246,10 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft
226246
}
227247
}
228248

229-
ngOnChanges(changes: SimpleChanges) {
230-
// Only update the inline SVG icon if the inputs changed, to avoid unnecessary DOM operations.
231-
const svgIconChanges = changes['svgIcon'];
232-
233-
this._svgNamespace = null;
234-
this._svgName = null;
235-
236-
if (svgIconChanges) {
237-
this._currentIconFetch.unsubscribe();
238-
239-
if (this.svgIcon) {
240-
const [namespace, iconName] = this._splitIconName(this.svgIcon);
241-
242-
if (namespace) {
243-
this._svgNamespace = namespace;
244-
}
245-
246-
if (iconName) {
247-
this._svgName = iconName;
248-
}
249-
250-
this._currentIconFetch = this._iconRegistry.getNamedSvgIcon(iconName, namespace)
251-
.pipe(take(1))
252-
.subscribe(svg => this._setSvgElement(svg), (err: Error) => {
253-
const errorMessage = `Error retrieving icon ${namespace}:${iconName}! ${err.message}`;
254-
this._errorHandler.handleError(new Error(errorMessage));
255-
});
256-
} else if (svgIconChanges.previousValue) {
257-
this._clearSvgElement();
258-
}
259-
}
260-
261-
if (this._usingFontIcon()) {
262-
this._updateFontIconClasses();
263-
}
264-
}
265-
266249
ngOnInit() {
267250
// Update font classes because ngOnChanges won't be called if none of the inputs are present,
268251
// e.g. <mat-icon>arrow</mat-icon> In this case we need to add a CSS class for the default font.
269-
if (this._usingFontIcon()) {
270-
this._updateFontIconClasses();
271-
}
252+
this._updateFontIconClasses();
272253
}
273254

274255
ngAfterViewChecked() {
@@ -430,5 +411,31 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft
430411
}
431412
}
432413

414+
/** Sets a new SVG icon with a particular name. */
415+
private _updateSvgIcon(rawName: string|undefined) {
416+
this._svgNamespace = null;
417+
this._svgName = null;
418+
this._currentIconFetch.unsubscribe();
419+
420+
if (rawName) {
421+
const [namespace, iconName] = this._splitIconName(rawName);
422+
423+
if (namespace) {
424+
this._svgNamespace = namespace;
425+
}
426+
427+
if (iconName) {
428+
this._svgName = iconName;
429+
}
430+
431+
this._currentIconFetch = this._iconRegistry.getNamedSvgIcon(iconName, namespace)
432+
.pipe(take(1))
433+
.subscribe(svg => this._setSvgElement(svg), (err: Error) => {
434+
const errorMessage = `Error retrieving icon ${namespace}:${iconName}! ${err.message}`;
435+
this._errorHandler.handleError(new Error(errorMessage));
436+
});
437+
}
438+
}
439+
433440
static ngAcceptInputType_inline: BooleanInput;
434441
}

tools/public_api_guard/material/icon.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export declare const MAT_ICON_LOCATION: InjectionToken<MatIconLocation>;
2323

2424
export declare function MAT_ICON_LOCATION_FACTORY(): MatIconLocation;
2525

26-
export declare class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, AfterViewChecked, CanColor, OnDestroy {
26+
export declare class MatIcon extends _MatIconMixinBase implements OnInit, AfterViewChecked, CanColor, OnDestroy {
2727
_svgName: string | null;
2828
_svgNamespace: string | null;
2929
get fontIcon(): string;
@@ -32,11 +32,11 @@ export declare class MatIcon extends _MatIconMixinBase implements OnChanges, OnI
3232
set fontSet(value: string);
3333
get inline(): boolean;
3434
set inline(inline: boolean);
35-
svgIcon: string;
35+
get svgIcon(): string;
36+
set svgIcon(value: string);
3637
constructor(elementRef: ElementRef<HTMLElement>, _iconRegistry: MatIconRegistry, ariaHidden: string, _location: MatIconLocation, _errorHandler: ErrorHandler);
3738
_usingFontIcon(): boolean;
3839
ngAfterViewChecked(): void;
39-
ngOnChanges(changes: SimpleChanges): void;
4040
ngOnDestroy(): void;
4141
ngOnInit(): void;
4242
static ngAcceptInputType_inline: BooleanInput;

0 commit comments

Comments
 (0)