common-ui/src/lib/dialog/base-dialog.component.ts

116 lines
4.0 KiB
TypeScript

import { AfterViewInit, Directive, HostListener, inject, OnDestroy, signal } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { debounceTime, firstValueFrom, fromEvent, merge, of, Subscription } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ConfirmOptions } from '.';
import { IconButtonTypes } from '../buttons';
import { LoadingService } from '../loading';
import { Toaster } from '../services';
import { hasFormChanged, IqserEventTarget } from '../utils';
import { ConfirmationDialogService } from './confirmation-dialog.service';
const DIALOG_CONTAINER = 'mat-dialog-container';
const TEXT_INPUT = 'text';
export interface SaveOptions {
closeAfterSave?: boolean;
addMembers?: boolean;
}
@Directive()
export abstract class BaseDialogComponent implements AfterViewInit, OnDestroy {
readonly #confirmationDialogService = inject(ConfirmationDialogService);
readonly #dialog = inject(MatDialog);
readonly #hasErrors = signal(true);
protected readonly _formBuilder = inject(UntypedFormBuilder);
protected readonly _loadingService = inject(LoadingService);
protected readonly _toaster = inject(Toaster);
protected readonly _subscriptions = new Subscription();
readonly iconButtonTypes = IconButtonTypes;
form?: UntypedFormGroup;
initialFormValue!: Record<string, string>;
protected constructor(
protected readonly _dialogRef: MatDialogRef<BaseDialogComponent>,
private readonly _isInEditMode = false,
) {}
get valid(): boolean {
return !this.form || this.form.valid;
}
get changed(): boolean {
return !this.form || hasFormChanged(this.form, this.initialFormValue);
}
get disabled(): boolean {
return !this.valid || !this.changed || this.#hasErrors();
}
ngAfterViewInit() {
this._subscriptions.add(this._dialogRef.backdropClick().subscribe(() => this.close()));
const valueChanges = this.form?.valueChanges ?? of(null);
const events = [fromEvent(window, 'keyup'), fromEvent(window, 'input'), valueChanges];
const events$ = merge(...events).pipe(
debounceTime(10),
tap(() => {
this.#hasErrors.set(!!document.getElementsByClassName('ng-invalid')[0]);
}),
);
this._subscriptions.add(events$.subscribe());
}
abstract save(options?: SaveOptions): void;
ngOnDestroy(): void {
this._subscriptions.unsubscribe();
}
close() {
if (!this._isInEditMode || !this.changed) {
return this._dialogRef.close();
}
this._openConfirmDialog().then(result => {
if (result) {
if (result === ConfirmOptions.CONFIRM) {
this.save({ closeAfterSave: true });
} else {
this._dialogRef.close();
}
}
});
}
@HostListener('window:keydown.Enter', ['$event'])
onEnter(event: KeyboardEvent): void {
const target = event.target as IqserEventTarget;
const isDialogSelected = target.localName?.trim()?.toLowerCase() === DIALOG_CONTAINER;
const isTextInputSelected = target.type?.trim()?.toLowerCase() === TEXT_INPUT;
if (
this.valid &&
!this.disabled &&
(this.changed || !this._isInEditMode) &&
this.#dialog.openDialogs.length === 1 &&
(isDialogSelected || isTextInputSelected)
) {
event?.stopImmediatePropagation();
this.save();
}
}
@HostListener('window:keydown.Escape', ['$event'])
onEscape(): void {
if (this.#dialog.openDialogs.length === 1) {
this.close();
}
}
protected _openConfirmDialog() {
const dialogRef = this.#confirmationDialogService.open({ disableConfirm: !this.valid });
return firstValueFrom(dialogRef.afterClosed());
}
}