How to build a reusable Modal Overlay/Dialog Using Angular CDK

mainawycliffe

Maina Wycliffe

Posted on November 20, 2019

How to build a reusable Modal Overlay/Dialog Using Angular CDK

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

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 and the GitHub repo for this post here.

Prerequisites

  • Install Angular CLI and Create an Angular Project – Link.

  • 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.

import {OverlayModule} from '@angular/cdk/overlay';
Enter fullscreen mode Exit fullscreen mode

And then, inject the Overlay service and ViewContainerRef into your component.

constructor(private overlay: Overlay, private viewContainerRef: ViewContainerRef) {}
Enter fullscreen mode Exit fullscreen mode

To show a modal overlay, you need either a Template or an Angular 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:

<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>
Enter fullscreen mode Exit fullscreen mode

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

openWithTemplate(tpl: TemplateRef<any>) {}
Enter fullscreen mode Exit fullscreen mode

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.

const configs = new OverlayConfig({
 hasBackdrop: true,
 backdropClass: 'modal-background'
});
Enter fullscreen mode Exit fullscreen mode

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

const overlayRef = this.overlay.create(configs);
Enter fullscreen mode Exit fullscreen mode

And then we can attach our template, using TemplatePortal, passing our template and ViewContainerRef which we injected into our component:

overlayRef.attach(new TemplatePortal(tpl, this.viewContainerRef));
Enter fullscreen mode Exit fullscreen mode

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

<button (click)="openWithTemplate(tpl)" >
 Show
</button>
Enter fullscreen mode Exit fullscreen mode

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.

this.overlayRef.attach(
 new ComponentPortal(OverlayComponent, this.viewContainerRef)
);
Enter fullscreen mode Exit fullscreen mode

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.

overlayRef.backdropClick().subscribe(() => overlayRef.dispose());
Enter fullscreen mode Exit fullscreen mode

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.

// 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 ) {
  
 }
 
}
Enter fullscreen mode Exit fullscreen mode

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.

afterClosed$ = new Subject<OverlayCloseEvent<R>>();
Enter fullscreen mode Exit fullscreen mode

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.

export interface OverlayCloseEvent<R> {
 type: 'backdropClick' | 'close';
 data: R;
}
Enter fullscreen mode Exit fullscreen mode

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.

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

  this.afterClosed$.complete();
 }
Enter fullscreen mode Exit fullscreen mode

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.

close(data?: R) {
 this._close('close', data);
}
Enter fullscreen mode Exit fullscreen mode

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

overlay.backdropClick().subscribe(() => this._close('backdropClick', null));
Enter fullscreen mode Exit fullscreen mode

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.

constructor(private ref: MyOverlayRef) {}
Enter fullscreen mode Exit fullscreen mode

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

contentType: 'template' | 'string' | 'component' = 'component';
content: string | TemplateRef<any> | Type<any>;
context;
Enter fullscreen mode Exit fullscreen mode

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

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';
    }
}
Enter fullscreen mode Exit fullscreen mode

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

close() {
 this.ref.close(null);
}
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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

<ng-container *ngTemplateOutlet="content; context: context"></ng-container>
Enter fullscreen mode Exit fullscreen mode

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

<ng-container *ngComponentOutlet="content"></ng-container>
Enter fullscreen mode Exit fullscreen mode

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.

export class OverlayService {
  constructor(private overlay: Overlay, private injector: Injector) {}
}
Enter fullscreen mode Exit fullscreen mode

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.

open<R = any, T = any>(
 content: string | TemplateRef<any> | Type<any>,data: T): MyOverlayRef<R> {
 
}
Enter fullscreen mode Exit fullscreen mode

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.

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

const overlayRef = this.overlay.create(configs);
Enter fullscreen mode Exit fullscreen mode

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

const myOverlayRef = new MyOverlayRef<R, T>(overlayRef, content, data);
Enter fullscreen mode Exit fullscreen mode

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

const injector = this.createInjector(myOverlayRef, this.injector);
Enter fullscreen mode Exit fullscreen mode

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

overlayRef.attach(new ComponentPortal(OverlayComponent, null, injector));
return myOverlayRef;
Enter fullscreen mode Exit fullscreen mode

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

createInjector(ref: MyOverlayRef, inj: Injector) {
 const injectorTokens = new WeakMap([[MyOverlayRef, ref]]);
 return new PortalInjector(inj, injectorTokens);
}
Enter fullscreen mode Exit fullscreen mode

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.

constructor(private overlayService: OverlayService) {}
Enter fullscreen mode Exit fullscreen mode

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.

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

ref.afterClosed$.subscribe(res => {
 console.log(res);
});
Enter fullscreen mode Exit fullscreen mode

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

String

const ref = this.overlayService.open("Hello World", null);
Enter fullscreen mode Exit fullscreen mode

Template

<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>
Enter fullscreen mode Exit fullscreen mode

And a button to open the modal:

<button (click)="open(tpl)" class="button is-small is-primary">
 Show
</button>
Enter fullscreen mode Exit fullscreen mode

And then open the overlay with an open method:

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

Component

open() {
 const ref = this.overlayService.open(YesNoDialogComponent, null);
 ref.afterClosed$.subscribe(res => {
  console.log(res);
 });
}
Enter fullscreen mode Exit fullscreen mode

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

constructor(private ref: MyOverlayRef) {}
Enter fullscreen mode Exit fullscreen mode

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

close(value: string) {
 this.ref.close(value);
}
Enter fullscreen mode Exit fullscreen mode

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

You can find all of the above code here and the demo here.

Additional Resources

💖 💪 🙅 🚩
mainawycliffe
Maina Wycliffe

Posted on November 20, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related