FontAwesome WebComponent <fa-icon>

In this post I'll describe my experiences with FontAwesome icon set and why I decided to build a WebComponent wrapper around their svg sprites.

tl;dr Source code is available on github

Problem description

Having a very good search for every icon I'd like use on FontAwesome website it was very convenient to experiment with some of them simply by searching and finding proper tag with right classes ready-to-use. Unfortunately with JavaScript replacement I lost every custom style I was trying to assign to it so I had to a wrapper element so any manipulations to original FontAwesome tag was not affecting me.

SVG sprites

Initially this wasn't a big deal but as I was closing all other topics this one stood on my list, so I decided to give it some more thoughts. First i checked what other options besides css/font-like usage are possible. This led me to the area of SVG sprites which allow embedding any icon inside svg tag using use tag. There are two variations:

  1. One small svg for each icon
  2. One big svg with uniquely named symbols.

SVG sprites seemed like a way to go but they had one drawback - I had to manually insert either path to smaller svg or a symbol id loosing all flexibility of having just css classs

After some more analysis I found out that those sprite id's are the EXACTLY the same as css class, which gave me an idea.

WebComponents to the rescue

WebComponents with its shadow dom allow proper separation from outside scope with respect to css which disables any unwanted changes in nested element.

Following SVG Sprites documentation on fontawesome docs my initial idea was to embed dynamically generated svg/use pair with proper selector so it can load proper icon. This worked great on chrome... while not on safari - probably because svg creates shadow dom for its children so it was a shadow dom in shadow dom.

So I thought that maybe good-old AJAX will help? Why AJAX (you might ask) - this 2018 and we've long forgotten this ugly very verbose XML syntax. And you'd be right - we do not exchange messages between servers in XML but SVG is an XML and after loading it from server it can be treated just like any other DOM tree with all its great JS semantics - there was only one way to find out :)

  1. Load SVG from server
  2. Build DOM tree on-the-fly
  3. Use getElementById to find proper sprite
  4. Copy sprite node into current document
  5. Insert copied sprite node into embedded svg

And this time it worked!

Elegant replacement for fa search ui

If you used fontawesome javascript replacement of html nodes you'll see how elegant this solution is - all you need to is just rename tag from i to fa-icon register fa-icon tag and you're ready to go!

Example usage

If you go to graduation cap icon, you see that it's possible to copy html containing css class for javascript replacement.

Below you can find the same icon using fa-icon tag

Custom styling

Having a regular html tags meets my expectations as to how it should behave when inherting from parent node or having custom css class applied to iself, see below:

Source code

The code is split into 3 pieces (full version on github)

Loading sprite from server

async function getSprite(type: string) { if (spriteCache.has(type)) { return spriteCache.get(type); } const retPromise = fetch(`${FONTAWESOME_LOCATION}/sprites/${type}.svg`) .then((res) => res.text()) .then((str) => new DOMParser().parseFromString(str, "text/xml")); spriteCache.set(type, retPromise); return retPromise; }

Getting proper sprite

private async buildSprite() { try { const svg = await getSprite(this.type); const iconSymbol = svg.getElementById(this.icon); if (!iconSymbol) { console.log('Symbol not found', this.className, this.type, this.icon); return; } const viewBox = iconSymbol.getAttribute('viewBox'); const path = iconSymbol.getElementsByTagName('path')[0]; this.attachShadow({mode: 'open'}).appendChild(this.getHtmlNode(viewBox, path)); } catch(e) { console.log('error building symbol'); } }

Injecting sprite into embedded shadow dom

private getHtmlNode(viewBox: string, path: Element) { const template = document.createElement('template'); template.innerHTML = ` ${path.outerHTML}`; return template.content.cloneNode(true); }

Waat 3MB xml???

After implemnting this whole solution I realized that there might be a little problem here - fontawesome xml's are big, very big!

But XML's are also great candidates for huffman coding so I wanted to see how good can it be? Below you can find image for both gzip brotli compression.

In case you'd like serve static assets compressed you can check my other article on Lambda@Edge with brotli encoding

Summary

As you can see in this example WebComponents is an easy and elegant solution for encapsulating custom bahviour and allows interactions using web-technologies in a way we're used to and with the help of modern tooling it solve our current problems in a better way.