DEV Community

Cover image for Homemade dialog service in Angular
Ayyash
Ayyash

Posted on • Originally published at garage.sekrab.com

Homemade dialog service in Angular

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

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

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

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);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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 the childRef!
  • Removed the onChildClose event. We can simply call the close method directly on the dialog 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[];
}
Enter fullscreen mode Exit fullscreen mode

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)