Trusted answers to developer questions
Trusted Answers to Developer Questions

Related Tags

angular
angularmaterial
typescript
html
scss

Angular Material Icon Component

Duncan Faulkner

Angular Materials Mat-Icon component

Using local SVG images.

Blog post #001

Duncan Faulkner - October 2019

When developing an Angular application with Angular Material, there comes a point when we need to add icons on our components or buttons, etc…

Angular Material has the Mat-Icon component for doing just that. This component works with web fonts like Font-Awesome For instance, by simply adding the name of the image required, an image is displayed.

For example:

<mat-icon>home</mat-icon>

Note: Requires an Angular application, Angular Material installed/configured, and a reference to a web font (like Font-Awesome library) all set up.

But what if you have custom icons that are not part of a web font, and you would like to make changes to them (e.g., change the color on hover or on a specific condition at runtime)?

In a recent project, I had a bespoke set of SVG icons. The Angular web application was to be installed on a server that didn’t have access to the internet, so the images had to be local. I wanted to use the Mat-Icon component out of the box (in an earlier version of the project, I had a custom icon component); and, I still wanted to be able to change the colors of the icons at various stages throughout the application based on certain conditions(like hover). This post covers how I achieved that.

SVG Icons

While there are a number of different ways to register an icon with the Mat-Icon component, this post discusses addSvgIcon. The others are addSvgIconInNamespace, addSvgIconLiteral, or addSvgIconLiteralInNamespace; all are methods of MatIconRegistry. I may try and cover these in more detail in a future post.

Setting up an Angular project

Note: In this post, I’m not going to step through the creation of an Angular application as there are so many already online. Plus, I tend to create Nx workspaces for all my Angular projects because I prefer that project layout. I plan to do a blog post on this very soon. For the moment ​though, see Getting started with Narwhal’s Nx Workspaces.

In the newly created Angular project, create a shared directory and add a new file named material.module.ts. I like to separate Angular Material imports into their own module and also create a separate module for other third-party components; this just makes it easier to import later, especially when using the Nx workspace layout and feature folders.

In the material.module.ts:

// Material Module example.
// All other Angular Material component imports here
// but the important ones are...
import {MatIconModule, MatIconRegistry} from '@angular/material/icon';

@NgModule({
	declarations: [],
	imports: [
	// Other material imports removed for brevity,
	MatIconModule],
	exports: [
	// Other material exports removed for brevity,
	MatIconModule, MatIconRegistry
	],
	entryComponents: [],
	providers: [MatIconRegistry]
})
export  class  MaterialModule  {}

MatIconModule is the module for the component, and MatIconRegistry is a service to register and display icons. Add a reference to the material.module.ts in the app.module.ts, and don’t forget to export it as well. Otherwise, it won’t be available, and Angular will not know anything about the Angular Material components.

// Include the material module in app.module.ts
import  {  BrowserModule  }  from  '@angular/platform-browser';
import  {  NgModule  }  from  '@angular/core';
import  {  AppComponent  }  from  './app.component';
import  {  BrowserAnimationsModule  }  from  '@angular/platform-browser/animations';
import  {  MaterialModule  }  from  './shared/material.module';

@NgModule({
declarations:  [AppComponent],
imports:  [BrowserModule,  BrowserAnimationsModule,  MaterialModule],
providers:  [],
bootstrap:  [AppComponent]
})
export  class  AppModule  {}

Now that we have the Angular Material components set up and configured, we need to register the icons before we can use them. For the moment, we’re just going to add these to the app.component.ts to get up and running – we’ll​ look at a better method later on.

// First Example
@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.scss']
})
export class AppComponent {
	constructor (
		private matIconRegistry: MatIconRegistery
	){
	this.matIconRegistry.addSvgIcon('home','assets/img/icon/home.svg');
	this.matIconRegistry.addSvgIcon('add','assets/img/icon/add.svg');
	}
	// or we could do this, and chain the addsvgIcon methods.
	// we'll use this method going forward in this post
	// {
	// 		this.matIconRegistry.addSvgIcon('home','assets/img/icon/home.svg')
	// 		.addSvgIcon('add','assets/img/icon/add.svg');
	// }
}

Add this to the app.component.html page (we’ll discuss this in more detail shortly).

<!-- First HTML example -->
<div>
	<mat-icon svgIcon="home"></mat-icon>
	<mat-icon svgIcon="add"></mat-icon>
</div>

At this point, we are not going to see much in the browser as we have an issue with the image URL. If you open the browsers console section, you will see the following error:

Error: unsafe value used in a resource URL context.

So what does this error mean? A brief explanation from the Angular Material website says:

“To prevent Cross-Site Scripting (XSS), SVG URLs and HTML strings passed to MatIconRegistry must be marked as trusted by the Angular’s DomSanitizer service. Icons are fetched via XmlHttpRequest and must have their URLs on the same domain as the containing page or configured to allow cross-domain access.”

So let’s add the DomSanitizer and fix this issue:

// Second Example - with the DomSanitizer
@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.scss']
})
export class AppComponent {
	constructor (
		private domSanitizer: DomSanitizer, 
		private matIconRegistry: MatIconRegistery
	){
	this.matIconRegistry
	.addSvgIcon('home',this.domSanitizer.bypassSecurityTrustResourceUrl('assets/img/icon/home.svg')
	.addSvgIcon('add',this.domSanitizer.bypassSecurityTrustResourceUrl('assets/img/icon/add.svg')
	// add other icons here....;
	}
}

The call to bypassSecurityTrustResourceUrl takes a URL as a parameter and sanitizes it so that an attacker cannot inject a JavaScript: URL, for example.

See the official documentation on DomSanitizer.

Now that we have this in place, we should see two icons in the browser.

If we have a lot of icons to add, this means lots of typing and repetitive code; so, lets refactor this some more. Start by removing all of this code (including the constructor) from the app.component.ts as it really shouldn’t be in the app.component.ts file. Lets create another new module in the shared directory, call it icon.module.ts, and add the following:

// Third Example - icon module
import  {  NgModule  }  from  '@angular/core';
import  {  DomSanitizer,  SafeResourceUrl  }  from  '@angular/platform-browser';
import  {  MaterialModule  }  from  '../shared/material.module';
import  {  MatIconRegistry  }  from  '@angular/material/icon';
@NgModule({
declarations:  [],
imports:  [MaterialModule],
exports:  [],
providers:  []
})
export  class  IconModule  {
private  path:  string  =  '../../assets/images'; // change this 
constructor(
private  domSanitizer:  DomSanitizer,
public  matIconRegistry:  MatIconRegistry
)  {
this.matIconRegistry
.addSvgIcon('home',  this.setPath(`${this.path}/home.svg`))
.addSvgIcon('add',  this.setPath(`${this.path}/file-plus.svg`));
}
private  setPath(url:  string):  SafeResourceUrl  {
	return  this.domSanitizer.bypassSecurityTrustResourceUrl(url);
	}
}

Overall, that’s not too bad. We are only writing out the domSanitizer code once in the private method, but, more importantly, all the code is out of the app.component.ts file and is now a self-contained module. If there are a lot of icons to add, then this file will get a bit long, but the typing has gotten shorter (well, a little shorter at least). You could change the constructor to iterate through a .json file of image names. The path wouldn’t change and could be a const, which would mean only maintaining a .json file for any new images. I may look at doing that in a follow-up post.

Note: Don’t forget to add this new icon.module.ts to the app.module.ts; otherwise, it won’t work.

Using the Mat-Icon component

So how do we use the mat-icon component? As seen earlier in this post, we add the following code to our app.components.html page:

<!-- First HTML example -->
<div>
	<mat-icon svgIcon="home"></mat-icon>
	<mat-icon svgIcon="add"></mat-icon>
</div>

This is a very simple example showing how to put a home and an add icon on a page/component. This is not too dissimilar to how we would use this component with web fonts, but we are now using the svgIcon input property. The value we give to this input is the first parameter used in our call to register the .addSvgIcon('home', ...), in this case, home.

Now, we have an icon in place, but how do we change the color of the icon when someone hovers over it?

Change the icon color

A Home icon

Example icon, copy this into a file with the SVG extension:

<svg version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" fill="#00FFFF" /></svg>

In the above XML, I’ve removed all the namespaces, etc. The main part here is the fill="#00FFFF". This sets the color for the image to, in this case, AquaMarine.

If the fill="#00..." property is not there, and you want a different color to the default black, then you can add it to the path above. This is optional.

I usually add the fill property, set it to white, and then change it in SCSS as (and when) required. Since this example had a white background, I used another color:

!<-- Second HTML example -->
<div>
	!<-- other code omitted for brevity -->	
	<a mat-button>
		<mat-icon svgIcon="home" class="btn-icon"></mat-icon>
	</a>
</div>

Add a class to the mat-icon html tag (as above). Next, add the SCSS for the class so that when a user hovers over the button, the icon’s color changes:

.btn-icon  {
	&:hover  {
		path {
			fill:  rgba(#00ffff,  1);
			}
		}
	}

Note: One thing I did forget to add to the app.component.ts above was the following line:

encapsulation:  ViewEncapsulation.None`

Without this, the hover affect doesn’t work:

@Component({
selector:  'mat-icon-demo-root',
templateUrl:  './app.component.html',
styleUrls:  ['./app.component.scss'],
encapsulation:  ViewEncapsulation.None,
})
export  class  AppComponent  {
	title  =  'mat-icon-demo';
}

Enjoy!

There is a GitHub repo for this blog post here.

RELATED TAGS

angular
angularmaterial
typescript
html
scss
RELATED COURSES

View all Courses

Keep Exploring