Browse Source

Merge pull request #19858 from abpframework/sinan/refactor-modal

Angular | Refactor modal component
pull/19959/head
Masum ULU 2 years ago
committed by GitHub
parent
commit
1917e6a57d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html
  2. 151
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts
  3. 4
      npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts

12
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html

@ -1,9 +1,9 @@
<ng-content></ng-content>
<ng-template #modalContent let-modal>
@if (abpHeader) {
@if (abpHeader()) {
<div id="abp-modal-header" class="modal-header abp-modal-header">
<ng-container *ngTemplateOutlet="abpHeader"></ng-container>
<ng-container *ngTemplateOutlet="abpHeader()"></ng-container>
​
<button
id="abp-modal-close-button"
@ -14,14 +14,14 @@
></button>
</div>
}
@if (abpBody) {
@if (abpBody()) {
<div id="abp-modal-body" class="modal-body">
<ng-container *ngTemplateOutlet="abpBody"></ng-container>
<ng-container *ngTemplateOutlet="abpBody()"></ng-container>
</div>
}
@if (abpFooter) {
@if (abpFooter()) {
<div id="abp-modal-footer" class="modal-footer">
<ng-container *ngTemplateOutlet="abpFooter"></ng-container>
<ng-container *ngTemplateOutlet="abpFooter()"></ng-container>
</div>
}
</ng-template>

151
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts

@ -1,20 +1,22 @@
import { SubscriptionService, uuid } from '@abp/ng.core';
import {
Component,
ContentChild,
EventEmitter,
Inject,
Input,
DestroyRef,
OnDestroy,
OnInit,
Optional,
Output,
TemplateRef,
ViewChild,
contentChild,
effect,
inject,
input,
model,
output,
viewChild,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgbModal, NgbModalOptions, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, takeUntil } from 'rxjs/operators';
import { fromEvent } from 'rxjs';
import { debounceTime, filter } from 'rxjs/operators';
import { Confirmation } from '../../models/confirmation';
import { ConfirmationService } from '../../services/confirmation.service';
import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '../../tokens/suppress-unsaved-changes-warning.token';
@ -30,66 +32,51 @@ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
providers: [SubscriptionService],
})
export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
@Input()
get visible(): boolean {
return this._visible;
}
set visible(value: boolean) {
if (typeof value !== 'boolean') return;
this.toggle$.next(value);
}
@Input()
get busy(): boolean {
return this._busy;
}
set busy(value: boolean) {
if (this.abpSubmit && this.abpSubmit instanceof ButtonComponent) {
this.abpSubmit.loading = value;
}
this._busy = value;
}
@Input() options: NgbModalOptions = {
keyboard: true,
};
protected readonly confirmationService = inject(ConfirmationService);
protected readonly modal = inject(NgbModal);
protected readonly modalRefService = inject(ModalRefService);
protected readonly suppressUnsavedChangesWarningToken = inject(SUPPRESS_UNSAVED_CHANGES_WARNING, {
optional: true,
});
protected readonly destroyRef = inject(DestroyRef);
@Input() suppressUnsavedChangesWarning = this.suppressUnsavedChangesWarningToken;
visible = model<boolean>(false);
@ViewChild('modalContent') modalContent?: TemplateRef<any>;
busy = input(false, {
transform: (value: boolean) => {
if (this.abpSubmit() && this.abpSubmit() instanceof ButtonComponent) {
this.abpSubmit().loading = value;
}
return value;
},
});
@ContentChild('abpHeader', { static: false }) abpHeader?: TemplateRef<any>;
options = input<NgbModalOptions>({ keyboard: true });
@ContentChild('abpBody', { static: false }) abpBody?: TemplateRef<any>;
suppressUnsavedChangesWarning = input(this.suppressUnsavedChangesWarningToken);
@ContentChild('abpFooter', { static: false }) abpFooter?: TemplateRef<any>;
modalContent = viewChild<TemplateRef<any>>('modalContent');
@ContentChild(ButtonComponent, { static: false, read: ButtonComponent })
abpSubmit?: ButtonComponent;
abpHeader = contentChild<TemplateRef<any>>('abpHeader');
@Output() readonly visibleChange = new EventEmitter<boolean>();
abpBody = contentChild<TemplateRef<any>>('abpBody');
@Output() readonly init = new EventEmitter<void>();
abpFooter = contentChild<TemplateRef<any>>('abpFooter');
@Output() readonly appear = new EventEmitter<void>();
abpSubmit = contentChild(ButtonComponent, { read: ButtonComponent });
@Output() readonly disappear = new EventEmitter<void>();
readonly init = output();
_visible = false;
readonly appear = output();
_busy = false;
readonly disappear = output();
modalRef!: NgbModalRef;
isConfirmationOpen = false;
destroy$ = new Subject<void>();
modalIdentifier = `modal-${uuid()}`;
private toggle$ = new Subject<boolean>();
get modalWindowRef() {
return document.querySelector(`ngb-modal-window.${this.modalIdentifier}`);
}
@ -98,17 +85,12 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
return Boolean(this.modalWindowRef?.querySelector('.ng-dirty'));
}
constructor(
private confirmationService: ConfirmationService,
private subscription: SubscriptionService,
@Optional()
@Inject(SUPPRESS_UNSAVED_CHANGES_WARNING)
private suppressUnsavedChangesWarningToken: boolean,
private modal: NgbModal,
private modalRefService: ModalRefService,
) {
this.initToggleStream();
constructor() {
effect(() => {
this.toggle(this.visible());
});
}
ngOnInit(): void {
this.modalRefService.register(this);
}
@ -116,7 +98,7 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
dismiss(mode: ModalDismissMode) {
switch (mode) {
case 'hard':
this.visible = false;
this.visible.set(false);
break;
case 'soft':
this.close();
@ -126,37 +108,29 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
}
}
private initToggleStream() {
this.subscription.addOne(this.toggle$.pipe(debounceTime(0), distinctUntilChanged()), value =>
this.toggle(value),
);
}
private toggle(value: boolean) {
this._visible = value;
this.visibleChange.emit(value);
protected toggle(value: boolean) {
this.visible.set(value);
if (!value) {
this.modalRef?.dismiss();
this.disappear.emit();
this.destroy$.next();
return;
}
setTimeout(() => this.listen(), 0);
this.modalRef = this.modal.open(this.modalContent, {
this.modalRef = this.modal.open(this.modalContent(), {
size: 'md',
centered: false,
keyboard: false,
scrollable: true,
beforeDismiss: () => {
if (!this.visible) return true;
if (!this.visible()) return true;
this.close();
return !this.visible;
return !this.visible();
},
...this.options,
windowClass: `${this.options.windowClass || ''} ${this.modalIdentifier}`,
...this.options(),
windowClass: `${this.options().windowClass || ''} ${this.modalIdentifier}`,
});
this.appear.emit();
@ -165,30 +139,27 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
ngOnDestroy(): void {
this.modalRefService.unregister(this);
this.toggle(false);
this.destroy$.next();
}
close() {
if (this.busy) return;
if (this.busy()) return;
if (this.isFormDirty && !this.suppressUnsavedChangesWarning) {
if (this.isFormDirty && !this.suppressUnsavedChangesWarning()) {
if (this.isConfirmationOpen) return;
this.isConfirmationOpen = true;
this.confirmationService
.warn(
'AbpUi::AreYouSureYouWantToCancelEditingWarningMessage',
'AbpUi::AreYouSure',
{ dismissible: false },
)
.warn('AbpUi::AreYouSureYouWantToCancelEditingWarningMessage', 'AbpUi::AreYouSure', {
dismissible: false,
})
.subscribe((status: Confirmation.Status) => {
this.isConfirmationOpen = false;
if (status === Confirmation.Status.confirm) {
this.visible = false;
this.visible.set(false);
}
});
} else {
this.visible = false;
this.visible.set(false);
}
}
@ -196,17 +167,17 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
if (this.modalWindowRef) {
fromEvent<KeyboardEvent>(this.modalWindowRef, 'keyup')
.pipe(
takeUntil(this.destroy$),
takeUntilDestroyed(this.destroyRef),
debounceTime(150),
filter((key: KeyboardEvent) => key && key.key === 'Escape' && this.options.keyboard),
filter((key: KeyboardEvent) => key && key.key === 'Escape' && this.options().keyboard),
)
.subscribe(() => this.close());
}
fromEvent(window, 'beforeunload')
.pipe(takeUntil(this.destroy$))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(event => {
if (this.isFormDirty && !this.suppressUnsavedChangesWarning) {
if (this.isFormDirty && !this.suppressUnsavedChangesWarning()) {
event.preventDefault();
}
});

4
npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts

@ -158,9 +158,9 @@ describe('ModalComponent', () => {
spectator.dispatchKeyboardEvent(spectator.component.modalWindowRef, 'keyup', 'Escape');
await wait300ms();
const { keyboard } = spectator.component.options;
const { keyboard } = spectator.component.options();
expect(spectator.component.visible).toBe(!keyboard);
expect(spectator.component.visible()).toBe(!keyboard);
});
it('should not close when busy is true', async () => {

Loading…
Cancel
Save