Intro
Every Front-End application includes a lot of media content. Today, I would like to talk about a small part of media content management - how to deal with SVG icons in your application. Why should you use SVG in your application? Well, there are some advantages in comparison with raster one:
- Scalability: SVG icons can shrink and grow at any size you want without quality loss;
- Customization. You can colorize your icons by simply adding a
color
CSS property and voilĂ - you have it; - Size: Usually, SVG icons are very tiny, and this fact positively impacts the vital performance metrics;
- Accessibility: Since SVG is XML-based, you can put their description or title for elements inside, which enhances the experience for people with disabilities and makes your application more friendly for search engines.
Problem statement
Let's look into the ways of using SVG icons in an application. Generally, we can handle it in different ways:
-
Use
background
CSS property. One of the easiest ways to use any type of content in an application; -
Create a script that will collect all of your icons and put them into an index.html file. Then, create a component that uses icons via
use
tag andhref
attribute like this:<svg> <use [attr.xlink:href]="'#app-icon-' + iconName" /> </svg>
This approach isn't obvious and can lead to performance issues since you can add some icons with large sizes, and they will be included (even if they aren't used) in your index.html
after an application build. This approach has been used for a long time in my current project. It became an issue when we added some icons with a size of about 350 KB, and it increased our index.html by more than three times (from 130KB to 430KB)
- Create a component that will load an icon from the icons folder when you need it by passing only an icon name. This solution will be described below in detail
Solution
There is a code of a shared component:
import { HttpClient } from '@angular/common/http';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { map, shareReplay } from 'rxjs/operators';
import { SvgIconService } from './svg-icon.service';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-svg-icon',
styleUrls: ['./svg-icon.component.less'],
// *1
template: ` <div [innerHTML]="sanitizedSvgContent"></div>`,
})
export class SvgIconComponent implements OnInit {
@Input() public iconName!: string;
public sanitizedSvgContent: SafeHtml;
constructor(
private cdr: ChangeDetectorRef,
private sanitizer: DomSanitizer,
private http: HttpClient,
private svgIconService: SvgIconService,
) {}
// *2
public ngOnInit(): void {
this.loadSvg();
}
// *3
private loadSvg(): void {
// Exit from the method in case of icon absence
if (!this.iconName) return;
// Construct your path to an icon
const svgPath = `/icons/svg/${this.iconName}.svg`;
// Check if the icon is already cached
if (!this.svgIconService.svgIconMap.has(svgPath)) {
// *4
const svg$ = this.http.get(svgPath, { responseType: 'text' }).pipe(
map((svg) => this.sanitizer.bypassSecurityTrustHtml(svg)),
shareReplay(1),
);
// Cache the result: iconName as a key and Observable as a value
this.svgIconService.svgIconMap.set(svgPath, svg$);
}
// Get an Observable with sanitized SVG from the Map
const cachedSvg$ = this.svgIconService.svgIconMap.get(svgPath);
// Subscribe to the Observable to get the content
cachedSvg$.subscribe(
(svg) => {
// Set it to the property
this.sanitizedSvgContent = svg;
// Trigger the 'detectChanges' method for UI updating
this.cdr.detectChanges();
},
// Simple error handling in case of any issue related to icon loading
(error) => console.error(`Error loading SVG`, error),
);
}
}
Some important comments for this code snippet:
- For the template, we use just
div
element with theinnerHTML
property, which allows us to insert any HTML as a string. In this place, we insert our SVG icons; - In the
ngOnInit
method we invoke theloadSvg
method, which handles the whole logic of getting a particular icon; - We must use caching to store already loaded icons. It will prevent unnecessary repeating loading (you can notice it when you use this component for repeated content generation - dashboard rows, tiles, list options, etc.). For this, we use a separate service
SvgIconService
with only one field -svgIconMap
which isMap
;
@Injectable({
providedIn: 'root',
})
export class SvgIconService {
public svgIconMap: Map<string, Observable<SafeHtml>> = new Map<string, Observable<SafeHtml>>();
}
- We use
HttpClient
for loading an icon by path. For operators, we usemap
function, which returns sanitized content (we simply skip checking this content since we trust the source. If you load icons from sources that you do not trust 100% - it's better to use thesanitize
function), andshareReplay
to ensure that the HTTP request is made only once, and future subscribers to thisObservable
will immediately retrieve an icon.
Let's see the styles for this component:
:host {
display: inline-block;
height: 18px;
width: 18px;
color: inherit;
}
div {
&::ng-deep svg {
display: block;
height: 100%;
width: 100%;
color: inherit;
fill: currentColor;
}
}
- We set the
width
and theheight
to ahost
element for the easiest way of size overriding - you can set any size you want outside by simply adding thewidth
andheight
properties; - For the SVG element, we set
color
andfill
for the same purpose - to get an opportunity for style override outside the component. Adding thedisplay
,height
, andwidth
properties ensures that the icon will take all available space.
With this solution, the index.html has been reduced from 430KB to 6KB, which
improved Page Load Time for 100-150ms in average.
Before using this component make sure that your icons are in the dist folder. You can make it by adding them to the assets property of the angular.json.
Usage example
Just add the new component by selector and pass an icon name as an input property:
<app-svg-icon icon="yourIconName"></app-svg-icon>
Make sure that you put the correct name and icon in the required folder. For size and color changes, you can add a class or an ID. Specify your rules and enjoy the result!
Conclusion
In this article, you have learned about a simple way of creating a shared Angular component for handling SVG icons. Remember that every application has its own specific and required application-fit solution. Hope you have found this article helpful and interesting.