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