Let's put all our knowledge in creating components programmatically in Angular 16 to good use. Let's create a Dialog service, that allows us to host any component. We already covered the major features in Rose Dialog in our StackBlitz project. Today we dig into the following issues to get closure:
- Style it to properly look like a dialog, and pass extra css
- Add extra event handlers to close
- Keep references of opened dialogs
- Housekeeping
Find the final Dialog service in StackBlitz project, under lib/Dialog
folder.
Styling
The sweetest part. We shall add a css file to the service, and style just enough to make it look right. In the wild, we should rely on a global stylesheet for coloring and measurements. We use Less or Sass for that, but I am leaving that part out to you. The idea is simple, make an overlay of semi transparent black, and host the dialog in the middle of it.
<!-- lib/Dialog/partial.html -->
<div class="dialog-overlay">
<div class="dialog">
<div class="dialog-header">
<h6 class="dialog-title" id="dialogtitle">{{ title }}</h6>
<button type="button" class="dialog-close" (click)="close()">❌</button>
</div>
<div class="dialog-body">
<ng-content></ng-content>
</div>
</div>
</div>
The bare minimum styles; which I promise will need more work, and may be done better within a couple of months as CSS quickly advances.
/* lib/Dialog/styles.css */
.dialog-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1030;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
background-color: #fff;
width: clamp(300px, 75vw, 90vw);
z-index: 1040;
overflow: hidden;
outline: 0;
display: flex;
flex-direction: column;
max-height: 90vh;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.2rem;
}
.dialog-body {
position: relative;
flex: 1 1 auto;
padding: 1.2rem;
overflow-y: auto;
}
Catch click and escape events
A nice feature to add is to hide the dialog when user clicks on the overlay or presses escape. We can add that directly to the dialog component.
// lib/Dialog/partial.ts
export class DialogPartialComponent {
// ...
// close on overlay click
@HostListener('click', ['$event.target'])
onClick(target: HTMLElement): void {
// find d-overlay (add it to the dialog-overlay)
if (target.matches('.d-overlay')) {
this.close(null);
}
}
// close on escape as well
@HostListener('window:keydown', ['$event'])
onEscape(event: KeyboardEvent): void {
// hide on escape
if (event.code === 'Escape') {
this.close(null);
}
}
}
I added a new class to the .dialog-overlay
named .d-overlay
, because I formed a habit long ago:
Never mix classes used in styles, with those used in scripts. It always bites back.
Add extra css
We can create a new option property to pass extra css to our dialog component, for maximum control.
// lib/Dialog/service
// add extra option in open method
public open(c: Type<any>, options?: {
// ...
css?: string,
) {
// ...
// get dialog element first
const dialogElement = (<EmbeddedViewRef<any>>componentRef.hostView)
.rootNodes[0];
// add css to the element root
if (options?.css) {
dialogElement.classList.add(...options.css.split(' '));
}
// then append
this.doc.body.append(dialogElement);
// ...
}
For example we can define our window to be a full screen dialog, then pass it like this
// pass a different cs
this.dialogService.open(MyPeachComponent, {
title: 'Peach',
css: 'dialog-full-screen'
});
I have included in StackBlitz some different styles, have a look at the Final
component. We can do something like this:
css: 'dialog-half-screen reverse animate fromright',
This would open a half screen dialog, and animates its position from the right.
Keep references of opened dialogs
It would be nice to be able to get a reference of an open dialog from anywhere in the app, by simply targeting its ID. Like this
// find the dialog by id and close it
this.dialogService.get('uniquePeach')?.close();
To implement that, we first need the ID to be a passable option. then we can collect them to a new property: dialogs
// lib/Dialog/service
// keep references of opened dialogs
dialogs: { [key: string]: DialogPartialComponent | null } = {};
public open(c: Type<any>, options?: {
//...
// add id
id?: string,
}) {
// get referecne to root element
const dialogElement = (<EmbeddedViewRef<any>>componentRef.hostView)
.rootNodes[0];
// and assign it the id
if (options?.id) {
dialogElement.id = options.id;
// add to collection
this.dialogs[options.id] = componentRef.instance;
}
// ...
// when closed destroy
const s = componentRef.instance.onClose.subscribe((res) => {
// get rid of reference
if (options?.id) {
delete this.dialogs[options.id];
}
// ..
});
// ..
}
// then get it
public get(id: string) {
// find the dialog ref component in collection
return this.dialogs[id] || null;
}
So next time we want to track a dialog, we simply pass it a unique ID. This probably needs a bit more work to guarantee uniqueness of ID, I'll let you work that out as you pleased.
Housekeeping
A little bit of housekeeping for the code includes the following
- Destroying the child: upon closing and destroying the
componentRef
, I forgot to destroy thechildRef
! - Removed the
onChildClose
event. We can simply call theclose
method directly on thedialog
reference passed. - Options need an interface:
IDialogOptions
. It now looks like this
// lib/Dialog/service
// the dialog options interface
export interface IDialogOptions {
title?: string;
data?: any;
css?: string;
id?: string;
onclose?: (res: any) => void;
providers?: StaticProvider[];
}
I could think of other enhancements to make, but that should do for now.
Note on the side, once you start using it,
ExpressionChangedAfterItHasBeenCheckedError
error will almost always fire, I do not bother much because I do not believe there is a solid solution for that. It is a development environment warning only.
Let's use it? Find the example in components FinalComponent
.
Thank you for reading this far, where you able to make use of this Dialog?
Top comments (0)