Friday, June 18, 2021

Font Awesome for Angular with Data-Driven Icons

Font Awesome is an easy-to-use library full of tons of images as fonts. There's also an Angular implementation for it, but it's a little trickier to use. We're using the Angular version and needed to be able to render icons based on data that would only be known at runtime. With vanilla Font Awesome you'd just bind the class based on the data and that's that, but with Angular Font Awesome you have to use a componentFactoryResolver to build the icon based on the data. There's technically documentation on exactly how to do this, but even with that it still took me quite a while to figure out exactly what needed to happen so I figured I'd write it up.

First things first, I was set on the right path by this SO answer so feel free to start there yourself or just keep reading. That answer links to this official documentation and between the two I was on my way, but still confused. The documentation says you just bind the icon you want (in the case of the example, faUser). That's not very dynamic and it's definitely not data-driven. What we need is a way to specify the prefix and the icon we want and having that render based on dynamic data. This issue on github shows that you can use the icon() function to generate an icon, but that wasn't working either. Eventually, through some significant debugging and source code review, I figured out that you can just pass an array to componentRef.instance.icon and your icon will show up, if you've already registered that icon in your icon library. Here's the final solution.

Step 1: Import the icons you're going to use. In our case we have a separate module to import all of the icons we want to use. We also have a pro license so we've got multiple icon types from Font Awesome to bring in. Here's what that module looks like.

import { NgModule } from '@angular/core';

import { FontAwesomeModuleFaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faSearch as falSearch } from '@fortawesome/pro-light-svg-icons'// fal
import { faCheckCirclefaShoppingCart as farShoppingCart } from '@fortawesome/pro-regular-svg-icons'// far
import {
  faBars,
  faBuilding,
  faHeadset,
  faHome,
  faSearch as fasSearch,
  faShoppingCart as fasShoppingCart,
  faTimes,
  faUnlock,
  faUser
from '@fortawesome/pro-solid-svg-icons'// fas
import { FontAwesomeIconHostComponent } from './components/font-awesome-host/font-awesome-host.component';

@NgModule({
  declarations: [ FontAwesomeIconHostComponent ],
  exports: [ FontAwesomeIconHostComponentFontAwesomeModule ]
})
export class CustomFontAwesomeModule {
  constructor(libraryFaIconLibrary) {
    library.addIcons(faBars);
    library.addIcons(faBuilding);
    library.addIcons(faCheckCircle);
    library.addIcons(faHeadset);
    library.addIcons(faHome);
    library.addIcons(falSearch);
    library.addIcons(farShoppingCart);
    library.addIcons(fasSearch);
    library.addIcons(fasShoppingCart);
    library.addIcons(faTimes);
    library.addIcons(faUnlock);
    library.addIcons(faUser);
  }
}


Step 2: Create a Font Awesome Host Component to build the icons dynamically based on the data
Update: I added a size property
import { ComponentComponentFactoryResolverInputOnInitViewChildViewContainerRef } from '@angular/core';

import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { IconNameIconPrefix } from '@fortawesome/fontawesome-svg-core';

@Component({
  selector: 'app-fa-host',
  template: '<ng-container #host></ng-container>'
})
export class FontAwesomeIconHostComponent implements OnInit {
  @ViewChild('host', {static: trueread: ViewContainerRef}) containerViewContainerRef;

  @Input() iconIconName;
  @Input() prefixIconPrefix;
  @Input() sizeSizeProp;

  constructor(private componentFactoryResolverComponentFactoryResolver) {
  }

  public ngOnInit(): void {
    this.createIcon();
  }

  public createIcon(): void {
    const factory = this.componentFactoryResolver.resolveComponentFactory(FaIconComponent);
    const componentRef = this.container.createComponent(factory);
    componentRef.instance.icon = [this.prefixthis.icon];
    componentRef.instance.size = this.size;
    // Note that FaIconComponent.render() should be called to update the
    // rendered SVG after setting/updating component inputs.
    componentRef.instance.render();
  }
}


Step 3: Use the Font Awesome Host Component and provide the icon and prefix based on the data
<atlas-fa-host [icon]="item.icon" [prefix]="item.iconPrefix" *ngIf="!!item.icon"></atlas-fa-host>

In this use case "item" is an object that has an icon property and an iconPrefix property. This still isn't perfect in my book because any time you add a row to the data that represents an icon you haven't imported yet, you'd have to update the Angular code to import it. But this does allow us to iterate through a list of objects and dynamically render an icon based on the data for each object. That's progress.