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:

Problem statement

Let's look into the ways of using SVG icons in an application. Generally, we can handle it in different ways:

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)

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:

  1. For the template, we use just div element with the innerHTML property, which allows us to insert any HTML as a string. In this place, we insert our SVG icons;
  2. In the ngOnInit method we invoke the loadSvg method, which handles the whole logic of getting a particular icon;
  3. 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 is Map;
@Injectable({
    providedIn: 'root',
})
export class SvgIconService {
    public svgIconMap: Map<string, Observable<SafeHtml>> = new Map<string, Observable<SafeHtml>>();
}
  1. We use HttpClient for loading an icon by path. For operators, we use map 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 the sanitize function), and shareReplay to ensure that the HTTP request is made only once, and future subscribers to this Observable 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;
    }
}

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.