How to build a reusable Modal Overlay/Dialog Using Angular CDK
Tags: angular, frontend, web, javascript
Published: 2019-11-20 00:44:41

Today, I am going to show you how to create a reusable modal overlay/dialog using Angular CDK Overlay that can be reused inside our Angular project multiple times with ease. We are going to try and match the behavior and the functionality of Angular Material Dialog component, but using the UI Framework of our choice.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/6f22ovog7oxjneg3hgct.gif)

This is going to be the first of a few articles, focusing on ways you can utilize Angular CDK to implement common interaction patterns within your application.

### Demo and Source Code

You can find the link to the demo [here](https://mainawycliffe.github.io/ng-cdk-overlay-demo/) and the GitHub repo for this post [here](https://github.com/mainawycliffe/ng-cdk-overlay-demo).

### Prerequisites

- Install Angular CLI and Create an Angular Project – [Link](https://codinglatte.com/posts/how-to/how-to-set-up-angular-for-beginners/).

- Setup Bulma inside your Angular Project.

- Install Angular CDK – `npm i @angular/cdk` or `yarn add @angular/cdk`

The article is divided into two sections:

- The Basics - a Quick look at how to use Angular CDK Overlay
- Building a Reusable Modal Overlay - A detailed guide into building a reusable
  modal overlay.

### The Basics

Let us get the basics out of the way first. Assuming you have installed Angular CDK you will need to import `OverlayModule` into your app module.

```typescript
import {OverlayModule} from '@angular/cdk/overlay';
```

And then, inject the `Overlay` service and [ViewContainerRef](https://angular.io/api/core/ViewContainerRef#description) into your component.

```typescript
constructor(private overlay: Overlay, private viewContainerRef: ViewContainerRef) {}
```

To show a modal overlay, you need either a [Template](https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/) or an Angular [Component](https://angular.io/api/core/Component) holding the content you would like to show. Let’s look at how you can use both below:

#### Using a Template

Inside the template of our component, let’s define a new template and add our overlay content:

```html
<ng-template #tpl>
  <div class="modal-card">
  <header class="modal-card-head">
    …
  </header>
  <section class="modal-card-body">
    …
  </section>
  <footer class="modal-card-foot">
    …
  </footer>
 </div>
</ng-template>
```

 Then, add a method to show the overlay and it accepts an `ng-template` reference as the parameter.

```typescript
openWithTemplate(tpl: TemplateRef<any>) {}
```

Then, Inside the above method, we are going to create an overlay. We will start by defining the configs of the overlay – `OverlayConfig`. In our case, we will just set the `hasBackdrop` and `backdropClass` properties. For `backdropClass` we are using `modal-background` a Bulma CSS Framework class. You can find all the Overlay Configurations you can add [here](https://material.angular.io/cdk/overlay/api#OverlayConfig).

```typescript
const configs = new OverlayConfig({
 hasBackdrop: true,
 backdropClass: 'modal-background'
});
```

Then, let’s create an `OverlayRef`, by using the `create` method of the Overlay service and pass the configs we just created above:

```typescript
const overlayRef = this.overlay.create(configs);
```

And then we can attach our template, using [TemplatePortal](https://material.angular.io/cdk/portal/api#TemplatePortal), passing our template and `ViewContainerRef` which we injected into our component:

```typescript
overlayRef.attach(new TemplatePortal(tpl, this.viewContainerRef));
```

Now, we can trigger the method with a click of a button:

```html
<button (click)="openWithTemplate(tpl)" >
 Show
</button>
```

#### Using a Component

The difference between the two is that we will be using `ComponentPortal` instead of `TemplatePortal` to attach the component to the `OverlayRef`.

```typescript
this.overlayRef.attach(
 new ComponentPortal(OverlayComponent, this.viewContainerRef)
);
```

**NB:** The component must be added to the list of `entryComponents` in your App Module.

##### Closing Modal Overlay on Backdrop Clip

You can close the overlay when the backdrop is clicked by subscribing to the `backdropClick()` and then calling the `dispose` method.

```typescript
overlayRef.backdropClick().subscribe(() => overlayRef.dispose());
```

### Making a Re-usable Modal Overlay

Building modal overlays as shown above works very well if you are building one or two of them, however, it does not scale very well. Wouldn’t it be nice if you could build a re-usable modal overlay that you can then use across your Angular project or projects? Wouldn’t it also be nice if we could be able to pass data to and receive data from the modal?

#### Objective

- A service to open the modal, than can be injected into any component

- A way to subscribe to when the modal is closed and access the response.

- Pass data to the modal

- Pass data as a String, Template or Component.

#### Overlay Ref Class

We are going to start by extending the `OverlayRef`. We will create a custom `OverlayRef`, creatively named `MyOverlayRef`, which is going to accept the `OverlayRef`, `content` and the data to pass to our modal overlay. The content can be of type `string`, `TemplateRef` or `Component`.

```typescript
// R = Response Data Type, T = Data passed to Modal Type
export class MyOverlayRef<R = any, T = any> {
 …
 constructor(public overlay: OverlayRef, public content: string | TemplateRef<any> | Type<any>, public data: T ) {
  …
 }
 …
}
```

 Then, Inside the `MyOverlayRef` class, we are going to add a `BehaviorSubject` property named `afterClosed$` which we can subscribe to get data once the overlay is closed.

```typescript
afterClosed$ = new Subject<OverlayCloseEvent<R>>();
```

The behavior subject is going to pass back an `OverlayCloseEvent`, which contains the data from modal and how the modal was closed. Feel free to modify this to cover your needs.

```typescript
export interface OverlayCloseEvent<R> {
 type: 'backdropClick' | 'close';
 data: R;
}
```

Next, we need to add a private method to close the overlay. The method will dispose off the overlay, pass the `OverlayCloseEvent` back to the subscriber and complete the `afterClosed$` Observable.

```typescript
private _close(type: 'backdropClick' | 'close', data: R) {
  this.overlay.dispose();
  this.afterClosed$.next({
   type,
   data
  });

  this.afterClosed$.complete();
 }
```

And then, we are going to add a second public close method. It will only accept `data` as a parameter and will call the private `_close` method, to close the modal.

```typescript
close(data?: R) {
 this._close('close', data);
}
```

And finally, we are going to subscribe to `backdropClick` and close the modal when clicked. We are adding this subscriber to the MyOverlayRef constructor.

```typescript
overlay.backdropClick().subscribe(() => this._close('backdropClick', null));
```

#### Overlay Component

Next, we are going to add a special component that we will use to show our modal content. If it is a simple string, we will bind it to a div, while we can use either `ngTemplateOutlet` and `ngComponentOutlet` to load the `template` and `component` respectively.

##### Component Class

We are going to start by injecting an instance of our `MyOverlayRef` into the component.

```typescript
constructor(private ref: MyOverlayRef) {}
```

And then, let’s define 3 more properties inside our component class:

```typescript
contentType: 'template' | 'string' | 'component' = 'component';
content: string | TemplateRef<any> | Type<any>;
context;
```

Then, `OnInit`, we need to determine the content type and set the above properties appropriately.

```typescript
ngOnInit() {
    if (typeof this.content === 'string') {
     this.contentType = 'string';
    } else if (this.content instanceof TemplateRef) {
     this.contentType = 'template';
     this.context = {
      close: this.ref.close.bind(this.ref)
     };
    } else {
     this.contentType = 'component';
    }
}
```

 And finally, we are going to be adding a global close button, so let’s add a `close` method:

```typescript
close() {
 this.ref.close(null);
}
```

 And finally, remember to add the Overlay component as an `entryComponent` in your app module.

##### Component Template

In the template, we are going to use `ngSwitch` to switch between content type and add a global close button for our modal.

```html
<div class="modal-content">
 <ng-container [ngSwitch]="contentType">
  <ng-container *ngSwitchCase="'string'">
      <div class="box">
        <div [innerHTML]="content"></div>
   </div>
  </ng-container>
  
  <ng-container *ngSwitchCase="'template'">
   …
  </ng-container>

  <ng-container *ngSwitchCase="'component'">
   …
  </ng-container>
 </ng-container>
</div>

<!-- You can also add a global close button -->
<button (click)="close()" class="modal-close is-large" aria-label="close"></button>
```

For template content type, we will use `ngTemplateOutlet`, passing the template, which is the `content` and then pass the `context`:

```html
<ng-container *ngTemplateOutlet="content; context: context"></ng-container>
```

And while for component content type, we will use `ngComponentOutlet`, passing the Component, which is the content:

```html
<ng-container *ngComponentOutlet="content"></ng-container>
```

#### The Overlay Service

Next, we are going to create an Overlay service, which we can inject into any component we want to use our modal overlay. Where we are going to inject `Overlay` Service and the `Injector`.

```typescript
export class OverlayService {
  constructor(private overlay: Overlay, private injector: Injector) {}
}
```

Then, add an `open` method, which will accept content and data. The `data` param, in this case, is the data you would like to pass to your modal.

```typescript
open<R = any, T = any>(
 content: string | TemplateRef<any> | Type<any>,data: T): MyOverlayRef<R> {
 …
}
```

 Inside the method, first, we need to create an `OverlayRef` object by using the `Overlay` `create` method. Feel free to customize the configs to suit your needs.

```typescript
const configs = new OverlayConfig({
 hasBackdrop: true,
 backdropClass: 'modal-background'
});

const overlayRef = this.overlay.create(configs);
```

Then, let’s instantiate our `MyOverlayRef` Class passing the `OverlayRef` object we created above, then content and the data, both from the method parameters.

```typescript
const myOverlayRef = new MyOverlayRef<R, T>(overlayRef, content, data);
```

Create an injector using `PortalInjector`, so that we can inject our custom `MyOverlayRef` object to the overlay component, we created above.

```typescript
const injector = this.createInjector(myOverlayRef, this.injector);
```

And finally use `ComponentPortal`, to attach the `OverlayComponent` we created above and the newly created injector and return a `MyOverlayRef` object.

```typescript
overlayRef.attach(new ComponentPortal(OverlayComponent, null, injector));
return myOverlayRef;
```

And here is the method for creating a custom injector using `PortalInjector`:

```typescript
createInjector(ref: MyOverlayRef, inj: Injector) {
 const injectorTokens = new WeakMap([[MyOverlayRef, ref]]);
 return new PortalInjector(inj, injectorTokens);
}
```

And that’s it, we now have a re-usable modal overlay, that we can use anywhere inside our application.

### Usage

First, inject the Overlay Service in the component where you would like to open a new modal overlay.

```typescript
constructor(private overlayService: OverlayService) {}
```

Then, inside the method you would want to trigger the modal overlay, you just
use the open method and pass the content and any data you want to pass to the
modal overlay.

```typescript
const ref = this.overlayService.open(content, null);

ref.afterClosed$.subscribe(res => {
 console.log(res);
});
```

The content can be a simple string or a `Template` or a `Component`.

#### String

```typescript
const ref = this.overlayService.open("Hello World", null);
```

#### Template

```html
<ng-template #tpl let-close="close">
  <div class="modal-card">
  <section class="modal-card-body">
   A yes no dialog, using template
  </section>
  <footer class="modal-card-foot">
   <div class="buttons">
    <button (click)="close('yes')" type="button" class="button is-success">Yes</button>
    <button (click)="close('no')" type="button" class="button is-danger">No</button>
   </div>
  </footer>
 </div>
</ng-template>
```

 And a button to open the modal:

```html
<button (click)="open(tpl)" class="button is-small is-primary">
 Show
</button>
```

And then open the overlay with an `open` method:

```typescript
open(content: TemplateRef<any>) {
 const ref = this.overlayService.open(content, null);
 ref.afterClosed$.subscribe(res => {
  console.log(res);
 });
}
```

#### Component

```typescript
open() {
 const ref = this.overlayService.open(YesNoDialogComponent, null);
 ref.afterClosed$.subscribe(res => {
  console.log(res);
 });
}
```

You can also inject the `MyOverlayRef` inside the component to access data and the close method.

```typescript
constructor(private ref: MyOverlayRef) {}
```

This allows you to pass data to the component and trigger the closing of the modal from within the component.

```typescript
close(value: string) {
 this.ref.close(value);
}
```

**NB:** Please remember to add the component as an `entryComponent` inside your App Module.

You can find all of the above code [here](https://github.com/mainawycliffe/ng-cdk-overlay-demo) and the demo [here](https://mainawycliffe.github.io/ng-cdk-overlay-demo/).

### Additional Resources

- [Creating Powerful Components with Angular CDK
](https://netbasal.com/creating-powerful-components-with-angular-cdk-2cef53d81cea)
- [API reference for Angular CDK overlay](https://material.angular.io/cdk/overlay/api)
- [Angular ng-template, ng-container and ngTemplateOutlet - The Complete Guide To Angular Templates](https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/)