[Angular] Custom icon component

·

2 min read

Hi, sometimes we don't want to implement huge library to use one small component.

In a simple example this is Material from Angular. When we decide to use only Material library then the problem doesn't exist, but when we want to use bootstrap or tailwind then the problems start.

In my case I will show you tailwind and MatIcon from Material.

Dependecies:

yarn add @material-design-icons/svg

The first step is to configure by adding assets in angular.json:

{
  "glob": "**/*.svg",
  "input": "node_modules/@material-design-icons/svg",
  "output": "assets/img/icons/material-design-icons/"
}

After this configuration, we have access to all svg files from the icon directory assets/img/icons/material-design-icons/STYLE/SOME_ICON.svg. Where:

  • STYLE is folder from node_modules/@material-design-icons/svg
  • SOME_ICON svg file

Any icon we fetch via Angular HttpClient as text and inject the response into the component using the ElementRef service. In this case, template or templateUrl are not necessary.

So after all the explanations, we have a simple component:

@Component({
  selector: 'app-icon',
  template: `<!-- empty -->`,
})
export class IconComponent implements OnInit, OnDestroy {
  @Input() class: string;
  @Input() icon: string;
  @Input()
  style: 'filled' | 'outlined' | 'round' | 'sharp' | 'two-tone' = 'two-tone';

  @Input() size: number;
  @Input() rem = 1;

  @HostBinding('style.width.rem') wRem = undefined;
  @HostBinding('style.height.rem') hRem = undefined;

  @HostBinding('attr.class')
  get buildClassList() {
    return (
      this.class +
      ` inline-block` +
      (this.size ? ` w-${this.size} h-${this.size}` : '')
    );
  }

  private subscription = new Subscription();

  constructor(
    private elRef: ElementRef<HTMLElement>,
    private httpClient: HttpClient
  ) {}

  ngOnInit(): void {
    if (!this.size) {
      this.hRem = this.wRem = this.rem;
    }
    this.subscription.add(
      this.httpClient
        .get(
          `assets/img/icons/material-design-icons/${this.style}/${this.icon}.svg`,
          {
            observe: 'body',
            responseType: 'text',
          }
        )
        .pipe(
          take(1),
          map((iconFileContent) =>
            iconFileContent
              .replace('width="24"', 'width="100%"')
              .replace('height="24"', 'height="100%"')
          )
        )
        .subscribe((res) => {
          this.elRef.nativeElement.innerHTML = res;
        })
    );
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

You can use this component with inputs:

  • class value will be copied
  • style value will be copied to the final class list and will be removed by overriding @HostBinding.
  • size (optional) with tailwindcss width and height (w-X h-X).
  • rem (optional, default: 1) width and height in rem

The default width and height in this case must be changed, so I decided to use replace strict values (only the first values found will be changed).

Thanks for reading :)