Lazy-loading Angular dialogs

In this blog I will share my technique for lazy-loading angular dialogs, to help reduce the initial file size of you application.

For this demonstration I will use the Angular CDK dialog for the base dialog functionality.

Adding lazy-loading

Let's start with this example, using the standard way of importing a dialog component and opening it.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Dialog } from '@angular/cdk/dialog';
import { DemoDialog } from './demo-dialog';

@Component({
  selector: 'demo-page',
  template: '<button (click)="openDialog()">Open dialog</button>',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DemoPage {
  private readonly _dialog = inject(Dialog);

  openDialog(): void {
    this._dialog.open(DemoDialog, { ... });
  }
}

Now to add the basic lazy-loading functionality we just need to make a few small changes. First we will make the openDialog function async, then we will move the DemoDialog component import to be inside the function.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Dialog } from '@angular/cdk/dialog';

@Component({
  selector: 'demo-page',
  template: '<button (click)="openDialog()">Open dialog</button>',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DemoPage {
  private readonly _dialog = inject(Dialog);

  // Convert to async
  async openDialog(): Promise<void> {
    // Await the component import
    const demoDialog = await import('./demo-dialog').then((c) => c.DemoDialog);
    this._dialog.open(demoDialog, { ... });
  }
}

Reducing boilerplate

Now having to remember to add this to any component where you want to open the dialog can get a bit much. So lets create a simple directive to go alonside the dialog component to make things simple.

We will take the openDialog function and move it to a directive, that wil then trigger it whenever the host element is clicked.

import { Directive, inject } from '@angular/core';
import { Dialog } from '@angular/cdk/dialog';

@Directive({
  selector: 'demoDialogTrigger',
  host: {
    "(click)": "openDialog()"
  }
})
export class DemoDialogTrigger {
  private readonly _dialog = inject(Dialog);

  async openDialog(): Promise<void> {
    const demoDialog = await import('./demo-dialog').then((c) => c.DemoDialog);
    this._dialog.open(demoDialog, { ... });
  }
}

Then we can use this directive to open the dialog.

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DemoDialogTrigger } from './demo-dialog';

@Component({
  selector: 'demo-page',
  // Import the directive
  imports: [DemoDialogTrigger],
  // Add the directive to an element
  template: '<button demoDialogTrigger>Open dialog</button>',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DemoPage {}

Make it configurable

Now we've simplified it and made it easy to use throughout your application lets extend it to allow for custom configuration.

We will do this adding an input the DemoDialogTrigger directive. Then using it in the openDialog function by merging it with any defaults that are set.

import { Directive, inject, input } from '@angular/core';
import { Dialog, DialogConfig } from '@angular/cdk/dialog';

@Directive({
  selector: 'demoDialogTrigger',
  host: {
    "(click)": "openDialog()"
  }
})
export class DemoDialogTrigger {
  private readonly _dialog = inject(Dialog);
  readonly config = input<DialogConfig<DemoData>>({})

  async openDialog(): Promise<void> {
    const demoDialog = await import('./demo-dialog').then((c) => c.DemoDialog);
    this._dialog.open(demoDialog, {
      ...,
      ...this.config()
    });
  }
}