• Skip to main content
  • Skip to primary sidebar

Web Development Archive

  • Archive
You are here: Home / Other / Dialog Service

Dialog Service

import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, ComponentType } from '@angular/cdk/portal';
import { inject, Injectable, InjectionToken, Injector } from '@angular/core';
import { Observable, Subject } from 'rxjs';

export const DIALOG_DATA = new InjectionToken<unknown>('DIALOG_DATA');

export class DialogRef<T> {
private readonly _afterClosed$ = new Subject<T | null>();

public constructor(private readonly _overlayRef: OverlayRef) {}

public close(result: T | null): void {
this._overlayRef.dispose();
this._afterClosed$.next(result);
this._afterClosed$.complete();
}

public afterClosed$(): Observable<T | null> {
return this._afterClosed$.asObservable();
}
}

@Injectable({
providedIn: 'root',
})
export class DialogService {
private readonly _overlay = inject(Overlay);
private readonly _rootInjector = inject(Injector);
private readonly _overlayRef: OverlayRef;

public open<T, D>(component: ComponentType<T>, data?: D): DialogRef<D | null> {
const overlayConfig = {
hasBackdrop: true,
backdropClass: 'cdk-overlay-dark-backdrop',
panelClass: 'modal-overlay-panel',
scrollStrategy: this._overlay.scrollStrategies.block(),
positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(),
};

const overlayRef = this._overlay.create(overlayConfig);
const dialogRef: DialogRef<D | null> = new DialogRef<D | null>(overlayRef);

const injector = this._createInjector(data, dialogRef);
const componentPortal = new ComponentPortal(component, null, injector);
overlayRef.attach(componentPortal);

overlayRef.backdropClick().subscribe(() => {
dialogRef.close(null);
});

return dialogRef;
}

public close(): void {
if (this._overlayRef) {
this._overlayRef.dispose();
}
}

private _createInjector<D>(data: D | null, dialogRef: unknown): Injector {
return Injector.create({
providers: [
{ provide: DIALOG_DATA, useValue: data },
{ provide: DialogRef, useValue: dialogRef },
],
parent: this._rootInjector,
});
}
}

Modal

import { animate, state, style, transition, trigger } from '@angular/animations';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
inject,
Input,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { DeviceService } from '@app/core/services';
import { ButtonComponent } from '@app/shared/components/button/button.component';
import { ButtonGroup, ButtonIcon, ButtonType } from '@app/shared/components/button/button.enum';
import { IconComponent } from '@app/shared/components/icon/icon.component';
import { TranslocoPipe } from '@jsverse/transloco';
import { Subject, takeUntil } from 'rxjs';

@Component({
selector: 'app-modal',
standalone: true,
imports: [CommonModule, ButtonComponent, IconComponent, TranslocoPipe],
templateUrl: './modal.component.html',
styleUrl: './modal.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('dialog', [
state('void', style({ opacity: 0, transform: 'scale(0.9)' })),
state('enter', style({ opacity: 1, transform: 'scale(1)' })),
transition('void => enter', [animate('100ms ease-out')]),
transition('enter => void', [animate('100ms ease-in')]),
]),
],
})
export class ModalComponent implements OnInit, OnDestroy {
private readonly _deviceService = inject(DeviceService);
private readonly _cdr = inject(ChangeDetectorRef);
private readonly _destroy$ = new Subject();
@Input() public isVisible: boolean = false;
@Input() public isMobile: boolean;
@Input() public isHeaderCloseVisible: boolean = true;
@Input() public header: string;
@Input() public btnPrimaryLabel?: string;
@Input() public btnSecondaryLabel?: string;
@Input() public isBtnPrimaryVisible?: boolean;
@Input() public isBtnSecondaryVisible?: boolean;
@Output() public isVisibleChange = new EventEmitter<boolean>();
@Output() public closeClick = new EventEmitter<void>();

@Output() public btnPrimaryClick = new EventEmitter<void>();
@Output() public btnSecondaryClick = new EventEmitter<void>();

public ButtonGroup = ButtonGroup;
public ButtonIcon = ButtonIcon;
public ButtonType = ButtonType;

public get classes(): string[] {
const deviceType = this.isMobile ? 'mobile' : 'desktop';
return [deviceType];
}

public ngOnInit(): void {
this._deviceService.isMobileObs$.pipe(takeUntil(this._destroy$)).subscribe((isMobile) => {
this.isMobile = isMobile;
this._cdr.markForCheck();
});
}

public ngOnDestroy(): void {
this._destroy$.next(null);
this._destroy$.complete();
}

public onClose(): void {
this.closeClick.emit();
this.isVisible = false;
}

public onBtnPrimary(): void {
this.btnPrimaryClick.emit();
}

public onBtnSecondary(): void {
this.btnSecondaryClick.emit();
}

public onOverlayClick(event: Event): void {
if ((event.target as HTMLElement).classList.contains('modal-overlay')) {
this.isVisibleChange.emit(false);
}
}
}
<div class="modal" [ngClass]="classes">
<div class="modal-header">
<h4 class="modal-header-title">{{ header }}</h4>
@if (isHeaderCloseVisible) {
<span class="modal-header-icon">
<app-icon [icon]="'icon-close'" [size]="24" (click)="onClose()" />
</span>
}
</div>
<div class="modal-body">
<ng-content select="[body]" />
</div>
<div
class="modal-footer"
*ngIf="isBtnPrimaryVisible || isBtnSecondaryVisible"
[ngClass]="{ flexend: !isBtnSecondaryVisible }"
>
<div class="modal-footer-button">
@if (isBtnPrimaryVisible) {
<app-button
class="btn-primary"
[label]="btnPrimaryLabel"
[buttonType]="ButtonType.PRIMARY"
[buttonGroup]="ButtonGroup.DEFAULT"
[buttonIcon]="ButtonIcon.DEFAULT"
[isWide]="true"
[isDisabled]="false"
[isDarkmode]="false"
(buttonClick)="onBtnPrimary()"
/>
}
</div>
<div class="modal-footer-button">
@if (isBtnSecondaryVisible) {
<app-button
class="btn-secondary"
*ngIf="btnSecondaryLabel"
[label]="btnSecondaryLabel"
[buttonType]="ButtonType.PRIMARY"
[buttonGroup]="ButtonGroup.OUTLINED"
[buttonIcon]="ButtonIcon.DEFAULT"
[isWide]="true"
[isDisabled]="false"
[isDarkmode]="false"
(buttonClick)="onBtnSecondary()"
/>
}
</div>
</div>
</div>

Use Dialog Service

<div class="modal" [ngClass]="classes">
<div class="modal-header">
<h4 class="modal-header-title">{{ header }}</h4>
@if (isHeaderCloseVisible) {
<span class="modal-header-icon">
<app-icon [icon]="'icon-close'" [size]="24" (click)="onClose()" />
</span>
}
</div>
<div class="modal-body">
<ng-content select="[body]" />
</div>
<div
class="modal-footer"
*ngIf="isBtnPrimaryVisible || isBtnSecondaryVisible"
[ngClass]="{ flexend: !isBtnSecondaryVisible }"
>
<div class="modal-footer-button">
@if (isBtnPrimaryVisible) {
<app-button
class="btn-primary"
[label]="btnPrimaryLabel"
[buttonType]="ButtonType.PRIMARY"
[buttonGroup]="ButtonGroup.DEFAULT"
[buttonIcon]="ButtonIcon.DEFAULT"
[isWide]="true"
[isDisabled]="false"
[isDarkmode]="false"
(buttonClick)="onBtnPrimary()"
/>
}
</div>
<div class="modal-footer-button">
@if (isBtnSecondaryVisible) {
<app-button
class="btn-secondary"
*ngIf="btnSecondaryLabel"
[label]="btnSecondaryLabel"
[buttonType]="ButtonType.PRIMARY"
[buttonGroup]="ButtonGroup.OUTLINED"
[buttonIcon]="ButtonIcon.DEFAULT"
[isWide]="true"
[isDisabled]="false"
[isDarkmode]="false"
(buttonClick)="onBtnSecondary()"
/>
}
</div>
</div>
</div>
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
inject,
OnDestroy,
OnInit,
} from '@angular/core';
import { DeviceService } from '@app/core/services';
import { DIALOG_DATA, DialogRef } from '@app/core/services/dialog.service';
import { FieldComponent } from '@app/shared/components/field/field.component';
import { FieldType } from '@app/shared/components/field/field.enum';
import { ModalComponent } from '@app/shared/components/modal/modal.component';
import { TranslocoPipe } from '@jsverse/transloco';
import { Subject, takeUntil } from 'rxjs';

import { StatsInfoDialogContentComponent } from '../stats-info-dialog-content/stats-info-dialog-content.component';

@Component({
selector: 'app-stats-info-dialog',
standalone: true,
imports: [ModalComponent, FieldComponent, TranslocoPipe, StatsInfoDialogContentComponent],
templateUrl: './stats-info-dialog.component.html',
styleUrl: './stats-info-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StatsInfoDialogComponent implements OnInit, OnDestroy {
private readonly _deviceService = inject(DeviceService);
private readonly _cdr = inject(ChangeDetectorRef);
private readonly _destroy$ = new Subject<void>();
public isMobile: boolean = false;
public data: string;
public FieldType = FieldType;

public constructor(
@Inject(DIALOG_DATA) dialogData: string,
private readonly _dialogRef: DialogRef<unknown>,
) {
this.data = dialogData;
}

public ngOnInit(): void {
this._deviceService.isMobileObs$.pipe(takeUntil(this._destroy$)).subscribe((isMobile) => {
this.isMobile = isMobile;
this._cdr.markForCheck();
});
}

public ngOnDestroy(): void {
this._destroy$.next();
this._destroy$.complete();
}

public onClose(): void {
if (this._dialogRef) {
this._dialogRef.close(null);
}
}
}

Filed Under: Other

About Gabor Flamich

I'm a web developer and designer based in Budapest, Hungary. In recent years, I've documented hundreds of solutions I came across during development. This site is an archive for useful code snippets on WordPress, Genesis Framework and WooCommerce. If You have any questions related to WordPress development, get in touch!

Primary Sidebar

  • angular.io
© 2026 WP Flames - All Right Reserved