DEV Community

Daniel Marin for This Dot

Posted on • Originally published at labs.thisdot.co on

Loading Components Dynamically in Angular 9 with Ivy

Loading Components Dynamically in Angular 9 with Ivy

This article will show you how to start loading components dynamically using Angular 9 with Ivy. This is not exactly new and exclusive to Angular 9, but now we can have components without a module, and by making them load dynamically, we get the benefits of lazy loading.

Making the story short, we'll reduce the main bundle size by loading only the components we need.

Imagine you have a huge module that consists of multiple components. Every user has unique needs, meaning that they will only use a subset of all the available components. The goal of this article is to explore a possible solution for addressing it.

To make it easier, I decided to work on a use case that I know.

visual representation of the application we are going to build

If you want to skip ahead, and go straight to the code, I created this repository with the finished version of the app. It looks like this:

The Problem

Let's say that we have this application, with which users can log in and perform some actions. Regardless of whether the user is a guest, or a registered user, they both have a profile page. Each kind of user has different actions that they can perform.

Solution

One way to solve this problem would be to use conditionals with the help of the ngIf structural directive. This allows us to have a different layout for each. It works, but is it the best solution? Remember that now both users have to download the entire component and actions, whether or not they use them.

I want to clarify, I have used the ngIf strategy in applications that have been in production for years.

Let's do something different this time. Let's create a component for each kind of user, and dynamically load them. This way, the main bundle won't have any of them, and they will be downloaded on demand.

Implementation

It's time to have fun. Before we start, make sure you have installed the Angular CLI v9. If you need help on this step, just drop a comment below. Once you have the right version of the Angular CLI installed, follow these steps:

  • Open your terminal of choice.
  • Run the command ng new {your-app-name}
  • Open the new project in your editor of choice.

Let's start with the loading of components. We are going to create a new service AppService. Once you've created it, open it in your editor src/app/app.service.ts and paste this:

import {
  Injectable,
  ComponentFactoryResolver,
  ViewContainerRef
} from '@angular/core';
import { from } from 'rxjs';
import { map } from 'rxjs/operators';

export interface ComponentLoader {
  loadChildren: () => Promise<any>;
}

@Injectable({
  providedIn: 'root'
})
export class AppService {
  constructor(private cfr: ComponentFactoryResolver) {}

  forChild(vcr: ViewContainerRef, cl: ComponentLoader) {
    return from(cl.loadChildren()).pipe(
      map((component: any) => this.cfr.resolveComponentFactory(component)),
      map(componentFactory => vcr.createComponent(componentFactory))
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, you see ComponentFactoryResolver, ViewContainerRef, ComponentLoader, and think:

What kind of sorcery is this?

It's simpler than you think. It's just that there are a few new things. We are injecting the ComponentFactoryResolver, which, given a Component, returns a Factory that can be used to create new instances of it. The ViewContainerRef is a pointer to an element in which we are going to insert the newly instantiated component. The ComponentLoader is a simple interface. It holds a loadChildren function that returns a Promise. This promise, once resolved, returns a Component.

And finally, we are just putting everything together. Using the from function from rxjs, I'm able to transform the promise into an observable. Then, I'm mapping this component into a factory, and finally I will inject the component, and return the instance.

Now, let's create another service named ProfileService that will use the AppService to load the respective component. It also holds the loggedIn state. Create a file in src/app/profile/profile.service.ts:

import { Injectable, ViewContainerRef } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { AppService } from '../app.service';

@Injectable({ providedIn: 'root' })
export class ProfileService {
  private isLoggedIn = new BehaviorSubject(false);
  isLoggedIn$ = this.isLoggedIn.asObservable();

  constructor(private appService: AppService) {}

  private guestProfile() {
    return () =>
      import('./guest-profile/guest-profile.component').then(
        m => m.GuestProfileComponent
      );
  }

  private clientProfile() {
    return () =>
      import('./client-profile/client-profile.component').then(
        m => m.ClientProfileComponent
      );
  }

  login() {
    this.isLoggedIn.next(true);
  }

  logout() {
    this.isLoggedIn.next(false);
  }

  loadComponent(vcr: ViewContainerRef, isLoggedIn: boolean) {
    vcr.clear();

    return this.appService.forChild(vcr, {
      loadChildren: isLoggedIn ? this.clientProfile() : this.guestProfile()
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

This service is way easier to understand. We created a Subject to manage the isLoggedIn state, and two methods to many events into the subject. We created two private methods that return a function that returns a Promise of a Component.

Yes, just like the ComponentLoader interface.

And finally, a magical method: loadComponent takes a ViewContainerRef and the isLoggedIn state. Clears the ViewContainerRef, emptying it entirely. Then, it calls the forChild method from AppService with the ViewContainerRef we just cleaned, and for the ComponentLoader, it has a ternary expression that determines which Component to load.

In order to make the loading of the components easier, we are going to create a directive that will help with that. Create a file src/app/profile/profile-host.directive.ts:

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({ selector: '[appProfileHost]' })
export class ProfileHostDirective {
  constructor(public viewContainerRef: ViewContainerRef) {}
}
Enter fullscreen mode Exit fullscreen mode

This is just a trick to make it easier to get the ViewContainerRef we're looking for. Now create a file src/app/profile/profile.component.ts:

import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ProfileHostDirective } from './profile-host.directive';
import { ProfileService } from './profile.service';
import { mergeMap, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-profile-container',
  template: `
    <ng-template appProfileHost></ng-template>
  `
})
export class ProfileComponent implements OnInit, OnDestroy {
  @ViewChild(ProfileHostDirective, { static: true })
  profileHost: ProfileHostDirective;
  private destroySubject = new Subject();

  constructor(private profileService: ProfileService) {}

  ngOnInit() {
    const viewContainerRef = this.profileHost.viewContainerRef;

    this.profileService.isLoggedIn$
      .pipe(
        takeUntil(this.destroySubject),
        mergeMap(isLoggedIn =>
          this.profileService.loadComponent(viewContainerRef, isLoggedIn)
        )
      )
      .subscribe();
  }

  ngOnDestroy() {
    this.destroySubject.next();
    this.destroySubject.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

All we are doing here is creating a simple ng-template in which we attach the ProfileHostDirective, so we can use the ViewChild decorator, and get the viewContainerRef. OnInit we are getting the viewContainerRef, and using the isLoggedIn$ observable from ProfileService to know everytime the isLoggedIn state changes. Then, using the mergeMap operator, I call the loadComponent function that is doing the real magic.

If you take a look at src/app/profile/profile.service.ts, you'll notice I'm referencing a GuestProfileComponent, and a ClientProfileComponent. Now it's time to create them.

First, go to the src/styles.scss, and include this:

html,
body {
  margin: 0;
  padding: 0;
}
Enter fullscreen mode Exit fullscreen mode

To make the styling easier, I created a folder style inside the assets folder, in which I have 2 scss files:

  • _variables.scss
  • _mixins.scss

They hold all the shared styles, to make everything easier to maintain:

// _variables.scss
$card-width: 400px;
$avatar-width: 80px;
$container-margin: 20px;

// _mixins.scss
@import './variables.scss';

@mixin button($color) {
  display: inline-block;
  padding: 0.5rem 1rem;
  border: 1px solid $color;
  border-bottom-color: darken($color, 10);
  border-radius: 5px;
  background: linear-gradient(180deg, $color, darken($color, 10));
  color: white;
  cursor: pointer;
  font-family: Arial, Helvetica, sans-serif;
  box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.2);
  font-size: 1rem;

  &:hover {
    background: $color;
    box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.2);
  }

  &:active {
    background: darken($color, 10);
  }
}

@mixin card {
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  border: 1px solid #eee;
  width: $card-width;
  padding: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

I also created a folder images, and included an image named profile.png. You can have any image as long as it's a square.

Let's create the GuestProfileComponent. For this, we'll need three files; a template, a stylesheet, and a typescript file. Let's start with the template: create a file src/app/profile/guest-profile/guest-profile.component.html

<section class="card">
  <div class="card__avatar">
    <div class="card __avatar__ head"></div>
    <div class="card __avatar__ body"></div>
  </div>

  <div class="container">
    <h2 class="card__title">Guest Profile</h2>

    <p class="card__subtitle">
      Thank you for visiting us. If you want to take your experience to the next
      level, all you need is to log in.
    </p>

    <div class="card__toolbar">
      <button (click)="login()">Login</button>
    </div>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Now let's create the stylesheet in src/app/profile/guest-profile/guest-profile.component.scss:

@import '~src/assets/styles/mixins.scss';

.card {
  display: flex;
  @include card();

  &__title {
    margin: 0 0 0.5rem 0;
  }

  &__subtitle {
    margin: 0 0 0.5rem 0;
  }

  &__toolbar button {
    @include button(#145092);
  }

  &__avatar {
    height: 80px;
    width: $avatar-width;
    border: 2px solid #bbb;
    background: #666;
    position: relative;
    overflow: hidden;

    &__head {
      position: absolute;
      border-radius: 50%;
      background: #bbb;
      width: 35px;
      height: 35px;
      top: 15px;
      left: 22px;
    }

    &__body {
      position: absolute;
      border-radius: 50%;
      background: #bbb;
      width: 70px;
      height: 50px;
      top: 55px;
      left: 5px;
    }
  }
}

.container {
  width: $card-width - $avatar-width - $container-margin;
  margin: 0 $container-margin;
}
Enter fullscreen mode Exit fullscreen mode

And finally, the typescript file in src/app/profile/guest-profile/guest-profile.component.ts:

import { Component } from '@angular/core';
import { ProfileService } from '../profile.service';

@Component({
  selector: 'app-guest-profile',
  templateUrl: './guest-profile.component.html',
  styleUrls: ['./guest-profile.component.scss']
})
export class GuestProfileComponent {
  constructor(private profileService: ProfileService) {}

  login() {
    this.profileService.login();
  }
}
Enter fullscreen mode Exit fullscreen mode

That's great! All we need to do now is create the ClientProfileComponent. We'll need the same files from the GuestProfileComponent. Let's start with the template src/app/profile/client-profile/client-profile.component.html

<section class="card">
  <figure class="card__avatar">
    <img src="assets/images/profile.png" />
  </figure>

  <h2 class="card__title" contenteditable="true">Daniel Marin</h2>

  <p class="card__subtitle" contenteditable="true">
    Senior Software Engineer at This Dot Labs, a company specializing in Modern
    Web Technologies, designing, and developing software to help companies
    maximize efficiency in their processes.
  </p>

  <div class="card__toolbar">
    <button (click)="logout()">Logout</button>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Now, let's create the stylesheet in src/app/profile/client-profile/client-profile.component.scss:

@import '~src/assets/styles/mixins.scss';

.card {
  @include card();

  &__avatar {
    height: $avatar-width;
    width: $avatar-width;
    margin: 0 auto;
    border-radius: 50%;
    overflow: hidden;

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  }

  &__title {
    margin: 1rem 0 0.5rem 0;
    text-align: center;
  }

  &__subtitle {
    margin: 0 0 1rem 0;
    text-align: center;
  }

  &__toolbar {
    display: flex;
    justify-content: center;

    button {
      @include button(#a80000);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And finally, the typescript file in src/app/profile/client-profile/client-profile.component.ts:

import { Component } from '@angular/core';
import { ProfileService } from '../profile.service';

@Component({
  selector: 'app-client-profile',
  templateUrl: './client-profile.component.html',
  styleUrls: ['./client-profile.component.scss']
})
export class ClientProfileComponent {
  constructor(private profileService: ProfileService) {}

  logout() {
    this.profileService.logout();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, all we have to do is update the AppComponent. Go to src/app/app.component.html, remove all its content, and put this instead:

<h1 class="header">Dynamic components</h1>
<main class="container">
  <app-profile-container></app-profile-container>
</main>
Enter fullscreen mode Exit fullscreen mode

Then, go to src/app/app.component.scss, and include this:

.header {
  background: #ddd;
  border-bottom: 1px solid #ccc;
  margin: 0;
  padding: 1rem;
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}

.container {
  display: flex;
  justify-content: center;
  margin-top: 2rem;
}
Enter fullscreen mode Exit fullscreen mode

Now, the only thing we cannot forget to do is add ProfileComponent, and ProfileHostDirective, to the AppModule declarations array. Go to src/app/app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ProfileHostDirective } from './profile/profile-host.directive';
import { ProfileComponent } from './profile/profile.component';

@NgModule({
  declarations: [AppComponent, ProfileHostDirective, ProfileComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

And we are done.

Conclusion

I hope that you had as much fun coding this as I did while writing this code. Now you know how to dynamically load components with lazy loading. With this knowledge, you can reduce the main bundle size, and make the experience better for your users. If you are having any problems, feel free to reach out to me via Twitter.

This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com]((https://www.thisdotlabs.com).

This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit thisdot.co.

Top comments (2)

Collapse
 
exequiel09 profile image
Exequiel Ceasar Navarrete • Edited

Hello, I was looking into this post. I just want to point out that in this line snippet of code above, the takeUntil should come last before anything else in the observable pipeline. See rxjs-no-unsafe-takeuntil rule in this package: npmjs.com/package/rxjs-tslint-rules. But still, awesome content! :)

 this.profileService.isLoggedIn$
      .pipe(
        takeUntil(this.destroySubject),
        mergeMap(isLoggedIn =>
          this.profileService.loadComponent(viewContainerRef, isLoggedIn)
        )
      )
      .subscribe();
Collapse
 
danmt profile image
Daniel Marin

You're right my friend!!! Very good catch, I should've used rxjs-tslist-rules...

Thanks for reading and sharing your feedback!!