31
loading...
This website collects cookies to deliver better user experience
MatDialog
, as that is a very commonly used service for creating modals.MatDialog
service allows you to pass a component to the service which it will then open in a floating dialog that's globally centered both horizontally and vertically. Obviously this dialog implements the Material Design spec including animations when opening and closing. Because of this, we want to implement our own design, but the ergonomics of the MatDialog
service are great. So our implementation, while not exactly the same, will be similar and provide some of the same features.open
method that takes in an Angular component to be opened in the dialog. We can also pass data to the component that can be used if needed. This method will return a reference to the dialog that we can use to close it programatically or subscribe to when it's closed. This API design is simple and easy to extend as needed, but gets us a highly functional dialog service.$ ng add @angular/material
$ npm install --save @angular/[email protected]
ComponentPortal
, then attach that portal to an OverlayRef
which we'll open.import { ComponentType } from '@angular/cdk/overlay';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DialogService {
constructor() {}
open<T>(component: ComponentType<T>) {
// 1. Create the overlay
// 2. Attach component portal to the overlay
}
}
open
method, and we know it needs to take some component to open. You'll notice we're using the type of ComponentType
from the Angular CDK overlay package. This is a type that allows us to receive any Angular component, and that's what is passed to the CDK when instantiating the component. Of course we also have our generic <T>
which will be the type of the component we pass through.GlobalPositionStrategy
. This means we won't be attaching it to a specific element. We also can provide a few more optional configuration options, which we'll do. Here's how we create that overlay, injecting the Overlay
class in the constructor:import { Overlay, ComponentType } from '@angular/cdk/overlay';
//...
export class DialogService {
constructor(private overlay: Overlay) {}
open<T>(component: ComponentType<T>) {
// Globally centered position strategy
const positionStrategy = this.overlay
.position()
.global()
.centerHorizontally()
.centerVertically();
// Create the overlay with customizable options
const overlayRef = this.overlay.create({
positionStrategy,
hasBackdrop: true,
backdropClass: 'overlay-backdrop',
panelClass: 'overlay-panel'
});
// Attach component portal to the overlay
}
}
src/styles.scss
.ComponentPortal
that we'll then attach to the overlay. It's quite straightforward, and we do it like so:import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
//...
export class DialogService {
constructor(private overlay: Overlay) {}
open<T>(component: ComponentType<T>) {
// Globally centered position strategy
// ...
// Create the overlay with customizable options
const overlayRef = this.overlay.create({
// ...
});
// Attach component portal to the overlay
const portal = new ComponentPortal(component);
overlayRef.attach(portal);
}
}
MatDialog
. We want to be able return a dialog reference so we can programmatically close the overlay or subscribe to when the overlay gets closed. So let's add that to our implementation.DialogRef
class. It should take in an OverlayRef
which we can use to close the overlay, and it should have an rxjs Subject
so we can subscribe to when the overlay is closed. So let's implement this simple class:import { OverlayRef } from '@angular/cdk/overlay';
import { Subject, Observable } from 'rxjs';
/**
* A reference to the dialog itself.
* Can be injected into the component added to the overlay and then used to close itself.
*/
export class DialogRef {
private afterClosedSubject = new Subject<any>();
constructor(private overlayRef: OverlayRef) {}
/**
* Closes the overlay. You can optionally provide a result.
*/
public close(result?: any) {
this.overlayRef.dispose();
this.afterClosedSubject.next(result);
this.afterClosedSubject.complete();
}
/**
* An Observable that notifies when the overlay has closed
*/
public afterClosed(): Observable<any> {
return this.afterClosedSubject.asObservable();
}
}
open
method so we can create this reference and return it from the method on creation. So let's put that in here:import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DialogRef } from './dialog-ref';
//...
export class DialogService {
constructor(private overlay: Overlay) {}
open<T>(component: ComponentType<T>): DialogRef {
// Globally centered position strategy
// ...
// Create the overlay with customizable options
const overlayRef = this.overlay.create({
// ...
});
// Create dialogRef to return
const dialogRef = new DialogRef(overlayRef);
// Attach component portal to the overlay
// ...
return dialogRef;
}
}
dialogRef
through? Well, for that we'll need to create an Injector which we pass to the component portal. This will allow us to then inject the dialogRef
in our component. It's pretty easy to do this, you can do it like so:import { Injectable, Injector } from '@angular/core';
import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DialogRef } from './dialog-ref';
//...
export class DialogService {
constructor(private overlay: Overlay, private injector: Injector) {}
open<T>(component: ComponentType<T>): DialogRef {
// Globally centered position strategy
// ...
// Create the overlay with customizable options
const overlayRef = this.overlay.create({
// ...
});
// Create dialogRef to return
const dialogRef = new DialogRef(overlayRef);
// Create injector to be able to reference the DialogRef from within the component
const injector = Injector.create({
parent: this.injector,
providers: [{ provide: DialogRef, useValue: dialogRef }]
});
// Attach component portal to the overlay
const portal = new ComponentPortal(component, null, injector);
overlayRef.attach(portal);
return dialogRef;
}
}
dialogRef
in our component very simply like so:@Component({
// ...
})
export class LoginComponent {
constructor(private dialogRef: DialogRef) {}
close() {
this.dialogRef.close();
}
}
dialogRef
. In this case, however, we'll need to define our own injection token for the dependency injection system. Let's start by doing that in a new file, dialog-tokens.ts
. It's going to be a very simple file.import { InjectionToken } from '@angular/core';
export const DIALOG_DATA = new InjectionToken<any>('DIALOG_DATA');
open
method to accept optional data to be passed to the component. As part of that, we'll define a DialogConfig
interface that has optional data
. The reason we're making this a config object like this is so it's easy to extend later if you wanted to, for example, allow customizing the options for the overlay.import { Injectable, Injector } from '@angular/core';
import { Overlay, ComponentType } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DialogRef } from './dialog-ref';
export interface DialogConfig {
data?: any;
}
//...
export class DialogService {
constructor(private overlay: Overlay, private injector: Injector) {}
open<T>(component: ComponentType<T>, config?: DialogConfig): DialogRef {
// Globally centered position strategy
// ...
// Create the overlay with customizable options
// ...
// Create dialogRef to return
// ...
// Create injector to be able to reference the DialogRef from within the component
const injector = Injector.create({
parent: this.injector,
providers: [
{ provide: DialogRef, useValue: dialogRef },
{ provide: DIALOG_DATA, useValue: config?.data }
]
});
// Attach component portal to the overlay
// ...
return dialogRef;
}
}
FlexibleConnectedPositionStrategy
that you can use to build things such as tooltips, popovers, dropdowns, and more. I'll be working hard on that one and hope to have it out soon. Hopefully this helps you get started digging into all the powerful options you have through the Angular CDK, and especially their Overlay package.