DEV Community

Cover image for Lazy-load a component in Angular without routing
Konstantinos Zagoris
Konstantinos Zagoris

Posted on • Originally published at wittyprogramming.dev

Lazy-load a component in Angular without routing

One of the most desirable features in Angular is to lazy load a component the time you need it. This approach provides many benefits to the loading speed of the application as it downloads only the required components when you need them. Furthermore, it is a very straightforward procedure through routing that is documented in the Angular docs. However, what if you do not want to use the router, or you want to lazy load a component programmatically through your code?

Scaffolding a Sample Form App

To highlight that scenario, let's create a minimal angular web app without routing with a button that shows a form when we click it. We will use, also the Angular Material to have a simple and beautiful design.

The application comprises two different components: the AppComponent and the LazyFormComponent.
The AppComponent shows the main app, which contains a button that shows the LazyFormComponent when pressed.

@Component({
  selector: "app-root",
  template: `
    <div style="text-align:center;margin-top: 100px;" class="content">
      <h1>Welcome to lazy loading a Component</h1>
      <button mat-raised-button color="primary" (click)="showForm = true">
        Load component form!
      </button>
      <app-lazy-form *ngIf="showForm"></app-lazy-form>
    </div>
  `,
  styles: [],
})
export class AppComponent {
  public showForm = false;
}
Enter fullscreen mode Exit fullscreen mode

The LazyFormComponent defines a simple reactive form with two inputs, a name and email, and a submit button:

@Component({
  selector: "app-lazy-form",
  template: `
    <form
      [formGroup]="simpleForm"
      style="margin:50px;"
      fxLayout="column"
      fxLayoutGap="20px"
      fxLayoutAlign="space-between center"
      (submit)="submitForm()"
    >
      <mat-form-field appearance="fill">
        <mat-label>Enter your Name</mat-label>
        <input matInput placeholder="John" formControlName="name" required />
        <mat-error *ngIf="name?.invalid">{{ getNameErrorMessage() }}</mat-error>
      </mat-form-field>
      <mat-form-field appearance="fill">
        <mat-label>Enter your email</mat-label>
        <input
          matInput
          placeholder="john@example.com"
          formControlName="email"
          required
        />
        <mat-error *ngIf="email?.invalid">{{
          getEmailErrorMessage()
        }}</mat-error>
      </mat-form-field>
      <button type="submit" mat-raised-button color="accent">Submit</button>
    </form>
  `,
  styles: [],
})
export class LazyFormComponent implements OnInit {
  simpleForm = new FormGroup({
    email: new FormControl("", [Validators.required, Validators.email]),
    name: new FormControl("", [Validators.required]),
  });

  get name() {
    return this.simpleForm.get("name");
  }

  get email() {
    return this.simpleForm.get("email");
  }

  constructor() {}

  ngOnInit(): void {}

  getNameErrorMessage() {
    if (this.name?.hasError("required")) {
      return "You must enter a value";
    }

    return this.email?.hasError("email") ? "Not a valid email" : "";
  }

  getEmailErrorMessage() {
    if (this.email?.hasError("required")) {
      return "You must enter a value";
    }

    return this.email?.hasError("email") ? "Not a valid email" : "";
  }

  submitForm() {
    if (this.email?.invalid || this.name?.invalid) return;
    alert("Form submitted successfully");
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, the AppModule glue everything together and imports the corresponding modules mainly for the Angular Material:

@NgModule({
  declarations: [AppComponent, LazyFormComponent],
  imports: [
    BrowserModule,
    MatButtonModule,
    BrowserAnimationsModule,
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
    FlexLayoutModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

The final result is:
Simple app workflow

Lazy Loading a Simple Component

What if we want to load the LazyFormComponent and their related material modules when we press the button and not the whole app?

We cannot use the route syntax to lazy load our component. Moreover, if we try to remove the LazyFormComponent from AppModule, the app fails because the Ivy compiler cannot find the required Angular Material modules needed for the form. This error leads to one of the critical aspects of Angular: The NgModule is the smallest reusable unit in the Angular architecture and not the Component, and it defines the component's dependencies.

There is a proposal to move many of these configurations to the component itself, making the use of NgModule optional. A very welcoming change that will simplify the mental model which programmers have on each angular application. But until that time, we need to create a new module for our LazyFormComponent, which defines its dependencies.

For a NgModule with one component, defining it in the same file with the component for simplicity is preferable.

So, the steps to display our lazy component is:

  • define where we want to load our component in the template with the ng-template tag,
  • define its view query through ViewChild decorator, which gives us access to the DOM and defines the container to which the component will be added,
  • finally, dynamic import the component and add it to the container

The AppComponent has transformed now as:

import {
  Component,
  ComponentFactoryResolver,
  ViewChild,
  ViewContainerRef,
} from "@angular/core";

@Component({
  selector: "app-root",
  template: `
    <div style="text-align:center;margin-top: 100px;" class="content">
      <h1>Welcome to lazy loading a Component</h1>
      <button mat-raised-button color="primary" (click)="loadForm()">
        Load component form!
      </button>
      <ng-template #formComponent></ng-template>
    </div>
  `,
  styles: [],
})
export class AppComponent {
  @ViewChild("formComponent", { read: ViewContainerRef })
  formComponent!: ViewContainerRef;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) {}

  async loadForm() {
    const { LazyFormComponent } = await import("./lazy-form.component");
    const componentFactory =
      this.componentFactoryResolver.resolveComponentFactory(LazyFormComponent);
    this.formComponent.clear();
    this.formComponent.createComponent(componentFactory);
  }
}
Enter fullscreen mode Exit fullscreen mode

For Angular 13

In Angular 13, a new API exists that nullifies the need for ComponentFactoryResolver. Instead, Ivy creates the component in ViewContainerRef without creating an associated factory. Therefore the code in loadForm() is simplified to:

export class AppComponent {
  @ViewChild("formComponent", { read: ViewContainerRef })
  formComponent!: ViewContainerRef;

  constructor() {}

  async loadForm() {
    const { LazyFormComponent } = await import("./lazy-form.component");
    this.formComponent.clear();
    this.formComponent.createComponent(LazyFormComponent);
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we added the LazyFormModule class:

@NgModule({
  declarations: [LazyFormComponent],
  imports: [
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
    BrowserAnimationsModule,
    FlexLayoutModule,
    MatButtonModule,
  ],
  providers: [],
  bootstrap: [LazyFormComponent],
})
export class LazyFormModule {}
Enter fullscreen mode Exit fullscreen mode

Everything seems to work fine:
Lazy-load app flow

Lazy loading a complex component

The above approach works for the simplest components, which do not depend on other services or components. But, If the component has a dependency, for example, a service, then the above approach will fail on runtime.

Let's say that we have a BackendService for our form submission form:

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

@Injectable()
export class BackendService {

    constructor() { }

    submitForm() {
        console.log("Form Submitted")
    }
}
Enter fullscreen mode Exit fullscreen mode

Moreover, this service needs to be injected in the LazyFormComponent:

constructor(private backendService: BackendService) {}

  submitForm() {
    if (this.email?.invalid || this.name?.invalid) return;
    this.backendService.submitForm();
    alert("Form submitted successfully");
  }
Enter fullscreen mode Exit fullscreen mode

But, when we try to lazy load the above component during runtime, it fails spectacularly:
Runtime error for lazy loading the component

Therefore, to make angular understand the need to load BackendService, the new steps are:

  • lazy load the module,
  • compile it to notify Angular about its dependencies,
  • finally, through the compiled module, we access the component and then add it to the container.

To access the component through the compiled module, we implement a helper function in the NgModule:

export class LazyFormModule {
  constructor(private componentFactoryResolver: ComponentFactoryResolver) {}

  getComponent() {
    return this.componentFactoryResolver.resolveComponentFactory(
      LazyFormComponent
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Therefore the code for lazy loading the LazyFormComponent on loadForm() function transforms to:

constructor(private compiler: Compiler, private injector: Injector) {}

  async loadForm() {
    const { LazyFormModule } = await import("./lazy-form.component");
    const moduleFactory = await this.compiler.compileModuleAsync(
      LazyFormModule
    );
    const moduleRef = moduleFactory.create(this.injector);
    const componentFactory = moduleRef.instance.getComponent();
    this.formComponent.clear();
    this.formComponent.createComponent(componentFactory, {ngModuleRef: moduleRef});
  }
Enter fullscreen mode Exit fullscreen mode

For Angular 13

Again, Angular 13 has simplified the above API. So now, the NgModule for the LazyFormComponent does not require injecting ComponentFactoryResolver. Therefore we only return the component:

export class LazyFormModule {
  constructor(private componentFactoryResolver: ComponentFactoryResolver) {}

  getComponent() {
    return LazyFormComponent
  }
}
Enter fullscreen mode Exit fullscreen mode

Furthermore, we do not need to inject the Compiler service because the compilation occurs implicitly with Ivy. So, instead of compiling the module, we only get the reference to it with the createNgModuleRef function:

constructor(private injector: Injector) {}

  async loadForm() {
    const { LazyFormModule } = await import("./lazy-form.component");
    const moduleRef = createNgModuleRef(LazyFormModule, this.injector)
    const lazyFormComponent = moduleRef.instance.getComponent();
    this.formComponent.clear();
    this.formComponent.createComponent(lazyFormComponent, {ngModuleRef: moduleRef});
  }
Enter fullscreen mode Exit fullscreen mode

Passing values and listening events

What if we want to pass some values or listen to some events from our lazy loading component? We cannot use the familiar syntax for a defined component in a template. Instead of that, we can access them programmatically.

For example, we want to change the text of the submit button on LazyFormComponent, and we want to be informed when the form is submitted. We add the required attributes, an Input() attribute for the prop buttonTitle and an Output() for the formSubmitted event:

export class LazyFormComponent implements OnInit {
  @Input()
  buttonTitle: string = "Submit";

  @Output() formSubmitted = new EventEmitter();

  submitForm() {
    if (this.email?.invalid || this.name?.invalid) return;
    this.backendService.submitForm();
    this.formSubmitted.emit();
    alert("Form submitted successfully");
  }
}
Enter fullscreen mode Exit fullscreen mode

The createComponent function returns an instance of the component which we can set the props and listen to the events through their observables:

formSubmittedSubscription = new Subscription();

 async loadForm() {
    const { LazyFormModule } = await import("./lazy-form.component");
    const moduleFactory = await this.compiler.compileModuleAsync(
      LazyFormModule
    );
    const moduleRef = moduleFactory.create(this.injector);
    const componentFactory = moduleRef.instance.getComponent();
    this.formComponent.clear();
    const { instance } = this.formComponent.createComponent(componentFactory, {ngModuleRef: moduleRef});
    instance.buttonTitle = "Contact Us";
    this.formSubmittedSubscription = instance.formSubmitted.subscribe(() =>
      console.log("The Form Submit Event is captured!")
    );
  }

    ngOnDestroy(): void {
        this.formSubmittedSubscription.unsubscribe();
    }
Enter fullscreen mode Exit fullscreen mode

You can check the complete sample solution in the GitHub repository here:

GitHub logo wittyprogramming / lazy-load-component-angular

Lazy load a component in Angular 12 without a router

Lazy-load a component in Angular without routing

One of the most seeking features in Angular is to lazy load a component when you need it. It is a very straightforward procedure through routing that is well documented. But, what if you do not want to use the router or you want to lazy load a component programmatically through your code?

Code for the following article: https://www.wittyprogramming.dev/articles/lazy-load-component-angular-without-routing/

Or the Angular 13 version:

GitHub logo wittyprogramming / lazy-load-component-angular13

Lazy load a component in Angular 13 without a router

Lazy-load a component in Angular 13 without routing

One of the most seeking features in Angular 13 is to lazy load a component when you need it. It is a very straightforward procedure through routing that is well documented. But, what if you do not want to use the router or you want to lazy load a component programmatically through your code?

Code for the following article: https://www.wittyprogramming.dev/articles/lazy-load-component-angular-without-routing/




Code-splitting and lazy-load components have their uses in modern web development, and I think with the changes in Angular 13, it has been simplified a lot.

Discussion (2)

Collapse
pxpp profile image
pxPP

Does anyone have any idea how I can enable routing in the loaded component ?

I have in the loaded module a router module for child and in the component a router outlet, but the routes are not loaded.

If I load the same component via the angular router lazy so the routing works.

Collapse
adisreyaj profile image
Adithya Sreyaj

Really good writeup 👍