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.
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.
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
“To prevent Cross-Site Scripting (XSS), SVG URLs and HTML strings passed to
MatIconRegistry
must be marked as trusted by the Angular’sDomSanitizer
service. Icons are fetched viaXmlHttpRequest
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.
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:
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?
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:
<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!