Lazy-load a component in Angular without routing
Tags: angular, javascript, typescript, tutorial
Published: 2021-11-28 14:10:36

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](https://angular.io/guide/lazy-loading-ngmodules). 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](https://material.angular.io/) 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.

```typescript
@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;
}
```

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

```typescript
@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");
  }
}
```

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

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

The final result is:
![Simple app workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8uvnqsrsg6p0y2qrao0.gif)

## 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](https://github.com/angular/angular/discussions/43784) 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](https://angular.io/api/core/ViewChild), 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:

```typescript
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);
  }
}
```

### 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:


```typescript
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);
  }
}
```
Finally, we added the `LazyFormModule` class:

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

Everything seems to work fine:
![Lazy-load app flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zcs2jwhsgp2go26ihyb0.gif)


## 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:

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

@Injectable()
export class BackendService {

    constructor() { }

    submitForm() {
        console.log("Form Submitted")
    }
}
```
Moreover, this service needs to be injected in the `LazyFormComponent`:

```typescript
constructor(private backendService: BackendService) {}

  submitForm() {
    if (this.email?.invalid || this.name?.invalid) return;
    this.backendService.submitForm();
    alert("Form submitted successfully");
  }
```
But, when we try to lazy load the above component during runtime, it fails spectacularly:
![Runtime error for lazy loading the component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2romck7gycyvl3fwpx80.png)

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`:


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

  getComponent() {
    return this.componentFactoryResolver.resolveComponentFactory(
      LazyFormComponent
    );
  }
}
```

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

```typescript
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});
  }
```
### 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:

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

  getComponent() {
    return LazyFormComponent
  }
}
```

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:

```typescript
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});
  }
```
## 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: 


```typescript
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");
  }
}
```

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


```typescript
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();
    }
```
You can check the complete sample solution in the GitHub repository here: 
{% github https://github.com/wittyprogramming/lazy-load-component-angular %}

Or the Angular 13 version:

{% github https://github.com/wittyprogramming/lazy-load-component-angular13 %}

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.