public

Ditch your icon fonts, dude.

Better icon management by using svgs with TypeScript. Gone are the days of having to create and manage your icon "font", tons of PNGs, or worse, variations of both!

Latest Post Speed kills software engineering. by Matthew Davis public
Better icon management by using svgs with TypeScript. Gone are the days of having to create and manage your icon "font", tons of PNGs, or worse, variations of both!

With standards increasing in capabilities, the traditional way of handling icons is going extinct. Try scaling, animating, and positioning that icon font, bad boy ;)

Let me introduce you to managing your icons using svgs and type safety.

Setup

First, install the node package svg-to-ts:

npm install svg-to-ts
Installation

Now, update your package.json by adding a scripts target for build:icons and then the svg-to-ts object below:

{
    "name": "@myorg/icons",
    "version": "0.0.0",
    "scripts": {
        "build:icons": "svg-to-ts-object",
    },
    "devDependencies": {
        "svg-to-ts": "^9.0.0",
        "typescript": "~4.8.4"
    },
    "svg-to-ts": {
        "srcFiles": [
            "./assets/icons/**/*.svg"
        ],
        "outputDirectory": "./src/app/shared/icons",
        "interfaceName": "Icon",
        "typeName": "Icon",
        "prefix": "icon",
        "svgoConfig": {
            "plugins": [
                "cleanupAttrs"
            ]
        },
        "fileName": "icons"
    }
}
Sample package.json

Running

Simply run npm run build:icons from your project root to generate your icons.ts file containing all of your svg's inlined:

Building the icons

After completion (1-2 seconds), your outputDirectory should contain an icon.ts like:

Outputs

Opening icons.ts will look something like this:

Implementing

Now that we have our SVGs inlined, typed, and optimized, we're ready to start using this in our view!

I created a custom component called app-icon which is just a wrapper around, ultimately injecting the string value of my Icons values to make things cleaner and quicker:

import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild, ViewContainerRef } from '@angular/core';
import tippy from 'tippy.js';
import Icons, { Icon } from './icons';

type IconSize = 6 | 8 | 10 | 12 | 14 | 16 | 20;

@Component({
    selector: 'app-icon',
    template: `
        <div #container class="flex gap-x-2.5 items-center" [class.h-full]="tooltip" [ngClass]="classes">
            <div #wrapper [ngStyle]="{ height: height + 'px', width: width + 'px' }"></div>
            <ng-content></ng-content>
        </div>
    `
})
export class IconComponent implements AfterViewInit {
    @ViewChild('container', { read: ViewContainerRef }) private container: ViewContainerRef;
    @ViewChild('wrapper', { read: ElementRef }) private svg: ElementRef;

    @Input() name: string;
    @Input() classes: string;
    @Input() tooltip: string;
    @Input() width: IconSize | number;
    @Input() height: IconSize | number;

    public constructor(private readonly changeDetectorRef: ChangeDetectorRef) {
    }

    public ngAfterViewInit() {
        this.width = this.width * 4;
        if (!this.height) {
            this.height = this.width;
        }
        this.svg.nativeElement.innerHTML = Icons[this.name as Icon];

        if (this.tooltip) {
            tippy(this.container.element.nativeElement, {
                theme: 'light',
                content: this.tooltip,
                placement: 'bottom',
                animation: 'shift-away-extreme',
                animateFill: true,
                followCursor: true,
                inertia: true,
                allowHTML: true
            });
        }
        this.changeDetectorRef.detectChanges();
    }
}
🀦
Don't forget to add IconComponent to your module's declarations: []!

Now you can start using your fancy icon library by dropping in:

<app-icon [name]="'bookmark'" [width]="8"></app-icon>
app.component.html

Remember, name is an @Input() of type Icon and takes a string value matching one of the values defined for each icon.

You can also reference the value typed as Icon programatically:

Because this is an svg you cannot just bind to [innerHTML] and hope it works.

You'll need to set this yourself like so:

import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild, ViewContainerRef } from '@angular/core';
import Icons, { Icon } from './icons';

@Component({
    selector: 'app-icon',
    template: `
        <div #wrapper style="width: 50px; height: 50px;"></div>
    `
})
export class IconComponent implements AfterViewInit {
    @ViewChild('wrapper', { read: ElementRef }) private svg: ElementRef;

    @Input() name: Icon;

    public constructor(private readonly changeDetectorRef: ChangeDetectorRef) {
    }

    public ngAfterViewInit() {
        this.svg.nativeElement.innerHTML = Icons[this.name as Icon];
        this.changeDetectorRef.detectChanges();
    }
}

See also

Matthew Davis

Published a year ago