Dynamically import module in Angular

Connie Leung - Jan 1 '22 - - Dev Community

The post is originally from http://www.blueskyconnie.com/?p=3181

Introduction

The elements of Spanish menu application https://github.com/railsstudent/ng-spanish-menu are primarily texts and buttons, and the user interface looks plain on first glance. I want to make it interesting by rendering an icon when quantity is below threshold.

This is the final output:

Display icon when quantity is low

The exclamation icon is loaded from angular-fontawesome followed by message, “Low Supply”.

I worked on the implementation twice:

Initially, static import FontAwesomeModule to the application, and used ng-if to conditionally render the icon and text. The solution had little code changes but the drawback was an additional of 32 kilobytes to the bundle size. The margin of increase is a lot considering the application is small and I am using only one icon of the library.

As the result of this discovery, the final design dynamically creates FaIconComponent and inserts it to an instance of ViewContainerRef. Then, inject Renderer2 and append “Low Supply” child to div parent.

This post will explain how I made the enhancement with the naive approach, what I discovered and the benefits of creating dynamic components in Angular.

Let's go

Install Angular Fontawesome in Angular

Firstly, we have to install angular-fontawesome schematics into the Angular application.

ng add @fortawesome/angular-fontawesome@0.9.0

Add font-awesome icon statically

Secondly, import FontAwesomeModule in food-choice module such that all icons are available to render in template.



food-choice.module.ts

import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'

import { FoodChoiceFormModule } from '../food-choice-form'
import { FoodChoiceComponent } from './food-choice.component'

@NgModule({
  declarations: [FoodChoiceComponent],
  imports: [CommonModule, FoodChoiceFormModule, FontAwesomeModule],
  exports: [FoodChoiceComponent],
})
export class FoodChoiceModule {}


Enter fullscreen mode Exit fullscreen mode

Thirdly, update component and template to display the icon and text conditionally.



// environment.ts
export const environment = {
  production: false,
  baseUrl: '/.netlify/functions',
  lowSupplyPercentage: 0.4,
}
// food-choice.component.ts

public ngOnInit(): void {
    this.remained = this.qtyMap ? this.qtyMap[this.choice.id] || 0 : 0
    this.minimumSupply = Math.ceil(this.remained * environment.lowSupplyPercentage)
}
// file-choice.component.html

<div class="flex items-center" *ngIf="remained > 0 && remained <= minimumSupply">
   <fa-icon [icon]="faExclamationTriangle" class="text-red-500 text-[1.35rem] mr-2"></fa-icon>
    <span class="text-red-500 text-xl">Low supply</span>
</div>


Enter fullscreen mode Exit fullscreen mode

Lastly, I examine the impacts of angular-fontawesome on the bundle size. The bundle size should increase but the degree of decrease is my major focus.

Install source-map-explorer to analyze the bundle of the project



npm i --save-dev source-map-explorer


Enter fullscreen mode Exit fullscreen mode

Build the project and enable source-map flag



ng build --source-map=true


Enter fullscreen mode Exit fullscreen mode

Bundle size after static import

Finally, analyze the source map to gather information on the size of different packages.



./node_modules/.bin/source-map-explorer ./dist/ng-spanish-menu/main.<hash sum>.js


Enter fullscreen mode Exit fullscreen mode

Source map after static import

The bottom right displays the size of angular-fontawesome and it is roughly the same size as rxjs. I have to improve the bundle size because one icon leads to a slightly bloated main.js.

Create dynamic fontawesome icon and text

This approach requires more steps than its counterpart but the bundle size shrinks eventually and the benefits outweigh the extra efforts.

Firstly, add a template reference (#lowSupplyRef) to the div parent. I will use the reference to append the “Low Supply” text later on.



// font-choice.template.html
<div class="flex items-center grow" #lowSupplyRef></div>


Enter fullscreen mode Exit fullscreen mode

Secondly, define a viewContainerRef inside the div element to host instances of font-awesome icon.



// font-choice.template.html
<div class="flex items-center grow" #lowSupplyRef>
   <ng-container #viewContainerRef></ng-container>
</div>


Enter fullscreen mode Exit fullscreen mode

Inside the component, declare a componentRef variable to hold a reference to font-awesome icon.



// food-choice.component.ts

public componentRef: ComponentRef<unknown> | null = null


Enter fullscreen mode Exit fullscreen mode

Use @ViewChild() decorator to obtain viewContainerRef and lowSupplierRef.



// food-choice.component.ts

@ViewChild('viewContainerRef', { read: ViewContainerRef, static: true })
public viewContainerRef: ViewContainerRef

@ViewChild('lowSupplyRef', { read: ElementRef, static: true })
public lowSupplierRef: ElementRef


Enter fullscreen mode Exit fullscreen mode

Next, define a function to create a dynamic font-awesome icon and insert it to viewContainerRef.



private async displayLowSupplyIcon() {
    const faExclamationTriangle = (await import('@fortawesome/free-solid-svg-icons')).faExclamationTriangle
    const FaIconComponent = (await import('@fortawesome/angular-fontawesome')).FaIconComponent
    const resolvedFaIconComponent = this.componentFactoryResolver.resolveComponentFactory(FaIconComponent)
    const faIconComponentRef = this.viewContainerRef.createComponent(resolvedFaIconComponent)
    faIconComponentRef.instance.icon = faExclamationTriangle
    faIconComponentRef.instance.classes = ['text-red-500', 'text-[1.35rem]', 'mr-2']
    faIconComponentRef.instance.render()
    this.componentRef = faIconComponentRef
}


Enter fullscreen mode Exit fullscreen mode

The first import() statement imports the exclamation icon.



const faExclamationTriangle = (await import('@fortawesome/free-solid-svg-icons')).faExclamationTriangle



Enter fullscreen mode Exit fullscreen mode

The next two lines of code create a FaIconComponent component.



const FaIconComponent = (await import('@fortawesome/angular-fontawesome')).FaIconComponent
const resolvedFaIconComponent = this.factoryResolver.resolveComponentFactory(FaIconComponent)


Enter fullscreen mode Exit fullscreen mode

Then, we create an instance of ComponentRef, assign the icon, specify tailwind CSS classes and render the svg.



const faIconComponentRef = this.viewContainerRef.createComponent(resolvedFaIconComponent)
faIconComponentRef.instance.icon = faExclamationTriangle
faIconComponentRef.instance.classes = ['text-red-500', 'text-[1.35rem]', 'mr-2']
faIconComponentRef.instance.render()
this.componentRef = faIconComponentRef


Enter fullscreen mode Exit fullscreen mode

Next, define another function to append the “Low Supply” text to lowSupplierRef.



private renderLowSupplyText() {
    const lowSupplySpanElement = this.renderer.createElement('span')
    lowSupplySpanElement.classList.add('text-red-500', 'text-xl')
    lowSupplySpanElement.innerText = 'Low Supply'
    this.renderer.appendChild(this.lowSupplierRef.nativeElement, lowSupplySpanElement)
}


Enter fullscreen mode Exit fullscreen mode

When quantity is low and icon has not rendered, render both icon and the text, and trigger change detection.



private async displayLowSupplyComponent() {
  if (!this.componentRef) {
     await this.displayLowSupplyIcon()
     this.renderLowSupplyText()
     this.cdr.detectChanges()
  }
}


Enter fullscreen mode Exit fullscreen mode

When quantity reaches zero, destroys the components and clears viewContainerRef to prevent memory leak.



private destroyComponents() {
    if (this.componentRef) {
      this.componentRef.destroy()
    }

    if (this.viewContainerRef) {
      this.viewContainerRef.clear()
    }

    Array.from(this.lowSupplierRef.nativeElement.children).forEach((child) => {
      this.renderer.removeChild(this.lowSupplierRef.nativeElement, child)
    })
}

private async handleLowSupply() {
    if (this.remained <= 0) {
      this.destroyComponents()
    } else if (this.remained > 0 && this.remained <= this.minimumSupply) {
      await this.displayLowSupplyComponent()
    }
}


Enter fullscreen mode Exit fullscreen mode

Finally, we call handleLowSupply() in ngOnInit and ngOnChanges.



public async ngOnInit(): Promise<void> {
    this.remained = this.qtyMap ? this.qtyMap[this.choice.id] || 0 : 0
    this.minimumSupply = Math.ceil(this.remained * environment.lowSupplyPercentage)

    await this.handleLowSupply()
}

public async ngOnChanges(changes: SimpleChanges): Promise<void> {
    ... omitted ...

    await this.handleLowSupply()
}


Enter fullscreen mode Exit fullscreen mode

You are almost there

Study the bundle size

We change many codes and keep the same user interface. Did the efforts significantly reduce the bundle size?

Re-run the commands below

ng build --source-map=true
./node_modules/.bin/source-map-explorer ./dist/ng-spanish-menu/main.<hash sum>.js

bundle size after dynamic import

source map after dynamic import

The bundle size increases by 3 kilobytes and angular-fontawesome library is removed from the source map.

Dynamic import does not add angular-fontawesome to main.js and instead, it splits into a couple of lazy chunk files (457.5da21ff230e58ed7c939.js and 859.106542046a8d67d7e411.js).

Final thoughts

Static import third-party library increases the bundle size of Angular application and importing a large library can contribute to a big bundle. In this example, the naive approach led to a 10% increase of the bundle size.

Thanks to dynamic import, ComponentFactoryResolver and ViewComponentRef classes, I can load icon on the fly, achieve the same result yet the bundle size increases by a few kilobytes.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other web technologies.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. ComponentFactoryResolver: https://angular.io/api/core/ComponentFactoryResolver
  3. ViewContainerRef: https://angular.io/api/core/ViewContainerRef
  4. Renderer2: https://angular.io/api/core/Renderer2
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .