Skip to content

fix(icon): not updating svg icon assigned through setter #20509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/material/icon/icon.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {
HttpTestingController,
TestRequest,
} from '@angular/common/http/testing';
import {Component, ErrorHandler} from '@angular/core';
import {Component, ErrorHandler, ViewChild} from '@angular/core';
import {MatIconModule, MAT_ICON_LOCATION} from './index';
import {MatIconRegistry, getMatIconNoHttpProviderError} from './icon-registry';
import {FAKE_SVGS} from './fake-svgs';
import {wrappedErrorMessage} from '@angular/cdk/testing/private';
import {MatIcon} from './icon';


/** Returns the CSS classes assigned to an element as a sorted array. */
Expand Down Expand Up @@ -64,6 +65,7 @@ describe('MatIcon', () => {
InlineIcon,
SvgIconWithUserContent,
IconWithLigatureAndSvgBinding,
BlankIcon,
],
providers: [
{
Expand Down Expand Up @@ -999,6 +1001,22 @@ describe('MatIcon', () => {

});

it('should handle assigning an icon through the setter', fakeAsync(() => {
iconRegistry.addSvgIconLiteral('fido', trustHtml(FAKE_SVGS.dog));

const fixture = TestBed.createComponent(BlankIcon);
fixture.detectChanges();
let svgElement: SVGElement;
const testComponent = fixture.componentInstance;
const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');

testComponent.icon.svgIcon = 'fido';
fixture.detectChanges();
svgElement = verifyAndGetSingleSvgChild(iconElement);
verifyPathChildElement(svgElement, 'woof');
tick();
}));

/** Marks an SVG icon url as explicitly trusted. */
function trustUrl(iconUrl: string): SafeResourceUrl {
return sanitizer.bypassSecurityTrustResourceUrl(iconUrl);
Expand Down Expand Up @@ -1089,3 +1107,8 @@ class SvgIconWithUserContent {
class IconWithLigatureAndSvgBinding {
iconName: string | undefined;
}

@Component({template: `<mat-icon></mat-icon>`})
class BlankIcon {
@ViewChild(MatIcon) icon: MatIcon;
}
101 changes: 54 additions & 47 deletions src/material/icon/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ import {
Inject,
InjectionToken,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewEncapsulation,
} from '@angular/core';
import {CanColor, CanColorCtor, mixinColor} from '@angular/material/core';
Expand Down Expand Up @@ -137,8 +135,8 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, AfterViewChecked,
CanColor, OnDestroy {
export class MatIcon extends _MatIconMixinBase implements OnInit, AfterViewChecked, CanColor,
OnDestroy {

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

/** Name of the icon in the SVG icon set. */
@Input() svgIcon: string;
@Input()
get svgIcon(): string { return this._svgIcon; }
set svgIcon(value: string) {
if (value !== this._svgIcon) {
if (value) {
this._updateSvgIcon(value);
} else if (this._svgIcon) {
this._clearSvgElement();
}
this._svgIcon = value;
}
}
private _svgIcon: string;

/** Font set that the icon is a part of. */
@Input()
get fontSet(): string { return this._fontSet; }
set fontSet(value: string) {
this._fontSet = this._cleanupFontValue(value);
const newValue = this._cleanupFontValue(value);

if (newValue !== this._fontSet) {
this._fontSet = newValue;
this._updateFontIconClasses();
}
}
private _fontSet: string;

/** Name of an icon within a font set. */
@Input()
get fontIcon(): string { return this._fontIcon; }
set fontIcon(value: string) {
this._fontIcon = this._cleanupFontValue(value);
const newValue = this._cleanupFontValue(value);

if (newValue !== this._fontIcon) {
this._fontIcon = newValue;
this._updateFontIconClasses();
}
}
private _fontIcon: string;

Expand Down Expand Up @@ -226,49 +246,10 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft
}
}

ngOnChanges(changes: SimpleChanges) {
// Only update the inline SVG icon if the inputs changed, to avoid unnecessary DOM operations.
const svgIconChanges = changes['svgIcon'];

this._svgNamespace = null;
this._svgName = null;

if (svgIconChanges) {
this._currentIconFetch.unsubscribe();

if (this.svgIcon) {
const [namespace, iconName] = this._splitIconName(this.svgIcon);

if (namespace) {
this._svgNamespace = namespace;
}

if (iconName) {
this._svgName = iconName;
}

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}`;
this._errorHandler.handleError(new Error(errorMessage));
});
} else if (svgIconChanges.previousValue) {
this._clearSvgElement();
}
}

if (this._usingFontIcon()) {
this._updateFontIconClasses();
}
}

ngOnInit() {
// Update font classes because ngOnChanges won't be called if none of the inputs are present,
// e.g. <mat-icon>arrow</mat-icon> In this case we need to add a CSS class for the default font.
if (this._usingFontIcon()) {
this._updateFontIconClasses();
}
this._updateFontIconClasses();
}

ngAfterViewChecked() {
Expand Down Expand Up @@ -430,5 +411,31 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Aft
}
}

/** Sets a new SVG icon with a particular name. */
private _updateSvgIcon(rawName: string|undefined) {
this._svgNamespace = null;
this._svgName = null;
this._currentIconFetch.unsubscribe();

if (rawName) {
const [namespace, iconName] = this._splitIconName(rawName);

if (namespace) {
this._svgNamespace = namespace;
}

if (iconName) {
this._svgName = iconName;
}

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}`;
this._errorHandler.handleError(new Error(errorMessage));
});
}
}

static ngAcceptInputType_inline: BooleanInput;
}
6 changes: 3 additions & 3 deletions tools/public_api_guard/material/icon.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export declare const MAT_ICON_LOCATION: InjectionToken<MatIconLocation>;

export declare function MAT_ICON_LOCATION_FACTORY(): MatIconLocation;

export declare class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, AfterViewChecked, CanColor, OnDestroy {
export declare class MatIcon extends _MatIconMixinBase implements OnInit, AfterViewChecked, CanColor, OnDestroy {
_svgName: string | null;
_svgNamespace: string | null;
get fontIcon(): string;
Expand All @@ -32,11 +32,11 @@ export declare class MatIcon extends _MatIconMixinBase implements OnChanges, OnI
set fontSet(value: string);
get inline(): boolean;
set inline(inline: boolean);
svgIcon: string;
get svgIcon(): string;
set svgIcon(value: string);
constructor(elementRef: ElementRef<HTMLElement>, _iconRegistry: MatIconRegistry, ariaHidden: string, _location: MatIconLocation, _errorHandler: ErrorHandler);
_usingFontIcon(): boolean;
ngAfterViewChecked(): void;
ngOnChanges(changes: SimpleChanges): void;
ngOnDestroy(): void;
ngOnInit(): void;
static ngAcceptInputType_inline: BooleanInput;
Expand Down