Skip to content

Instantly share code, notes, and snippets.

@Nakira
Last active May 25, 2025 12:45
Show Gist options
  • Select an option

  • Save Nakira/84875f5892107e8489c3824ab31c7b5b to your computer and use it in GitHub Desktop.

Select an option

Save Nakira/84875f5892107e8489c3824ab31c7b5b to your computer and use it in GitHub Desktop.
import {
Directive,
ElementRef,
Renderer2,
OnInit,
AfterViewInit,
inject,
} from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { DialogHelperService } from './DialogHelperService';
@Directive()
export abstract class BaseAsyncDialogComponent<T = any, R = any>
implements OnInit, AfterViewInit
{
private el = inject(ElementRef);
private renderer = inject(Renderer2);
constructor(
protected dialogRef: MatDialogRef<T, R>,
protected data: any // add this!
) {}
protected dialogHelper = inject(DialogHelperService);
ngOnInit() {
if (!this.dialogRef) return;
this.dialogRef.keydownEvents().subscribe(event => {
if (event.key === 'Escape') {
this.tryClose('close');
}
});
}
ngAfterViewInit(): void {
if (!this.dialogRef) return;
const backdropClass = `.${this.data.dialogInstanceId}`;
requestAnimationFrame(() => {
const backdrop = document.querySelector(backdropClass);
if (backdrop) {
this.renderer.listen(backdrop, 'click', () => {
this.tryClose('close');
});
}
});
}
/**
* Override this to define what payload to return (or false to cancel).
*/
protected abstract getClosePayload(action: string): Promise<R | false>;
/**
* Try to close the dialog using the action string.
*/
async tryClose(action: string): Promise<void> {
const result = await this.getClosePayload(action);
if (result !== false) {
this.dialogRef.close(result);
this.dialogHelper.releasePosition(); // toggle side after this dialog closes
}
}
}
import { Injectable, ComponentRef } from '@angular/core';
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { ComponentType } from '@angular/cdk/portal';
@Injectable({ providedIn: 'root' })
export class DialogHelperService {
private openDialogsCount = 0;
private nextDialogRightSide: boolean = true;
constructor(private matDialog: MatDialog) {}
togglePosition() {
this.nextDialogRightSide = !this.nextDialogRightSide;
}
getNextPosition(): { right?: string; left?: string } {
return this.nextDialogRightSide ? { right: '0' } : { left: '0' };
}
// called from BaseAsyncDialogComponent on close
releasePosition() {
this.togglePosition();
}
openDialog<T, D = any, R = any>(
component: ComponentType<T>,
data: D,
configOverride: Partial<MatDialogConfig<D>> = {}
): MatDialogRef<T, R> {
const position = this.openDialogsCount % 2 === 0 ? { right: '0' } : { left: '0' };
const dialogConfig = new MatDialogConfig<D>();
const dialogId = `dialog-${Date.now()}-${Math.random().toString(16).slice(2)}`;
dialogConfig.position = position;
dialogConfig.backdropClass = dialogId;
dialogConfig.data = { ...data, dialogInstanceId: dialogId };
dialogConfig.width = '90vw';
dialogConfig.height = '100vh';
dialogConfig.maxWidth = '100vw';
dialogConfig.maxHeight = '100vh';
dialogConfig.minWidth = '90vw';
dialogConfig.autoFocus = false;
dialogConfig.disableClose = true;
dialogConfig.panelClass = 'custom-dialog-container';
Object.assign(dialogConfig, configOverride);
const ref = this.matDialog.open<T, D, R>(component, dialogConfig);
this.openDialogsCount++;
ref.afterClosed().subscribe(() => {
this.openDialogsCount = Math.max(0, this.openDialogsCount - 1);
});
return ref;
}
}

Async Dialog Framework for Angular Material

This utility provides an elegant system to manage Angular Material dialogs with:

  • Controlled async closing (Promise-based)
  • Auto-payload resolution via getClosePayload()
  • Automatic Escape and backdrop click handling
  • Alternating left/right dialog positions
  • Support for dialog and non-dialog contexts (e.g., routed pages)

Contents

  • BaseAsyncDialogComponent<T, R> – base class for components that act as async dialogs
  • DialogHelperService – helper to open dialogs with consistent styling and behavior

Installation

Copy the following files into your Angular project:

  • base-async-dialog.component.ts
  • dialog-helper.service.ts

Then inject DialogHelperService wherever you want to open dialogs.


Usage

1. Extend BaseAsyncDialogComponent

@Component({ /* ... */ })
export class MyDialogComponent extends BaseAsyncDialogComponent<MyDialogComponent, MyCloseResult> {
  constructor(
    @Optional() @Inject(MAT_DIALOG_DATA) override data: any,
    @Optional() dialogRef: MatDialogRef<MyDialogComponent>
  ) {
    super(dialogRef, data);
  }

  override async getClosePayload(action: string): Promise<MyCloseResult | false> {
    if (action === 'cancel') return false;

    const result = await this.saveIfNeeded();
    return {
      action,
      data: result
    };
  }
}


---

2. Open a Dialog

constructor(private dialogHelper: DialogHelperService) {}

openDialog() {
  this.dialogHelper
    .openDialog(MyDialogComponent, { inputData: { id: 123 } })
    .afterClosed()
    .subscribe(result => {
      console.log('Dialog closed with:', result);
    });
}


---

3. Close Dialog from Inside

Inside MyDialogComponent, call:

this.tryClose('save');   // Calls getClosePayload('save')
this.tryClose('cancel'); // Can be blocked by returning `false`


---

4. Alternating Left/Right Side

Dialogs opened via DialogHelperService will alternate side (right/left) automatically.

You can override this manually:

this.dialogHelper.openDialog(MyDialogComponent, data, {
  position: { left: '0' }
});


---

Features

Automatically adds a unique dialogInstanceId and backdropClass

Dialog close is delayed until getClosePayload() resolves

ESC key and backdrop click are intercepted and routed through tryClose()

Works with or without MAT_DIALOG_DATA and MatDialogRef

Stack-safe alternating layout logic



---

Advanced Tips

Combine with Angular’s @Input() + withComponentInputBinding() to reuse components in both dialogs and routes

If you need to distinguish between dialog vs. standalone mode, check this.dialogRef



---

License

MIT  use freely in commercial or open-source projects.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment