Homemade dialog service in Angular
Tags: angular, typescript, webdev, tutorial
Published: 2023-06-06 07:10:11

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](https://stackblitz.com/edit/angular-program-component) 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](https://stackblitz.com/edit/angular-program-component?file=src%2Flib%2FDialog%2Fservice.ts) 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.

```html
<!-- 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.

```css
/* 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.

```ts
// 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.

```ts
// 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

```ts
// pass a different cs
this.dialogService.open(MyPeachComponent, {
  title: 'Peach',
  css: 'dialog-full-screen'
});
```

I have included in [StackBlitz](https://stackblitz.com/edit/angular-program-component) 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

```ts
// 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`

```ts
// 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 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

```ts
// 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?