Efficient SVG Icon Management in Angular: A Comprehensive Guide Using Angular Components

Written by amirankou | Published 2023/12/20
Tech Story Tags: web-development | angular | components | icon | svg | rxjs | svg-icons | angular-component

TLDRvia the TL;DR App

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 and href 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:

  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;
    }
}
  • We set the width and the height to a host element for the easiest way of size overriding - you can set any size you want outside by simply adding the width and height properties;
  • For the SVG element, we set color and fill for the same purpose - to get an opportunity for style override outside the component. Adding the display, height, and width 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.


Written by amirankou | Passionate about Front-End development
Published by HackerNoon on 2023/12/20