feat: use modal service to project modal

pull/3552/head
Arman Ozak 6 years ago
parent 146c863160
commit 0cf7d1d261

@ -1,34 +1,43 @@
<div
*ngIf="visible"
[@fade]="isModalOpen"
id="modal-container"
class="modal show {{ modalClass }}"
tabindex="-1"
role="dialog"
>
<div class="modal-backdrop" (click)="close()"></div>
<ng-template #template>
<div
id="abp-modal-dialog"
class="modal-dialog modal-{{ size }}"
role="document"
[class.modal-dialog-centered]="centered"
#abpModalContent
*ngIf="visible"
[@fade]="isModalOpen"
id="modal-container"
class="modal show {{ modalClass }}"
tabindex="-1"
role="dialog"
>
<div id="abp-modal-content" class="modal-content">
<div id="abp-modal-header" class="modal-header">
<ng-container *ngTemplateOutlet="abpHeader"></ng-container>
<button id="abp-modal-close-button" type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div id="abp-modal-body" class="modal-body">
<ng-container *ngTemplateOutlet="abpBody"></ng-container>
</div>
<div id="abp-modal-footer" class="modal-footer">
<ng-container *ngTemplateOutlet="abpFooter"></ng-container>
<div class="modal-backdrop" (click)="close()"></div>
<div
id="abp-modal-dialog"
class="modal-dialog modal-{{ size }}"
role="document"
[class.modal-dialog-centered]="centered"
#abpModalContent
>
<div id="abp-modal-content" class="modal-content">
<div id="abp-modal-header" class="modal-header">
<ng-container *ngTemplateOutlet="abpHeader"></ng-container>
<button
id="abp-modal-close-button"
type="button"
class="close"
aria-label="Close"
(click)="close()"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div id="abp-modal-body" class="modal-body">
<ng-container *ngTemplateOutlet="abpBody"></ng-container>
</div>
<div id="abp-modal-footer" class="modal-footer">
<ng-container *ngTemplateOutlet="abpFooter"></ng-container>
</div>
</div>
</div>
</div>
<ng-content></ng-content>
</div>
</ng-template>
<ng-content></ng-content>

@ -1,3 +1,4 @@
import { takeUntilDestroy } from '@abp/ng.core';
import {
Component,
ContentChild,
@ -12,10 +13,11 @@ import {
ViewChildren,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, filter, takeUntil } from 'rxjs/operators';
import { fadeAnimation } from '../../animations/modal.animations';
import { Confirmation } from '../../models/confirmation';
import { ConfirmationService } from '../../services/confirmation.service';
import { ModalService } from '../../services/modal.service';
import { ButtonComponent } from '../button/button.component';
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
@ -33,20 +35,7 @@ export class ModalComponent implements OnDestroy {
}
set visible(value: boolean) {
if (typeof value !== 'boolean') return;
this.isModalOpen = value;
this._visible = value;
this.visibleChange.emit(value);
if (value) {
setTimeout(() => this.listen(), 0);
this.renderer.addClass(document.body, 'modal-open');
this.appear.emit();
} else {
this.renderer.removeClass(document.body, 'modal-open');
this.disappear.emit();
this.destroy$.next();
}
this.toggle$.next(value);
}
@Input()
@ -79,6 +68,8 @@ export class ModalComponent implements OnDestroy {
@ContentChild('abpClose', { static: false, read: ElementRef })
abpClose: ElementRef<any>;
@ViewChild('template', { static: false }) template: TemplateRef<any>;
@ViewChild('abpModalContent', { static: false }) modalContent: ElementRef;
@ViewChildren('abp-button') abpButtons;
@ -101,11 +92,43 @@ export class ModalComponent implements OnDestroy {
destroy$ = new Subject<void>();
private toggle$ = new Subject<boolean>();
get isFormDirty(): boolean {
return Boolean(document.querySelector('.modal-dialog .ng-dirty'));
}
constructor(private renderer: Renderer2, private confirmationService: ConfirmationService) {}
constructor(
private renderer: Renderer2,
private confirmationService: ConfirmationService,
private modalService: ModalService,
) {
this.initToggleStream();
}
private initToggleStream() {
this.toggle$
.pipe(takeUntilDestroy(this), debounceTime(0), distinctUntilChanged())
.subscribe(value => this.toggle(value));
}
private toggle(value: boolean) {
this.isModalOpen = value;
this._visible = value;
this.visibleChange.emit(value);
if (value) {
this.modalService.renderTemplate(this.template);
setTimeout(() => this.listen(), 0);
this.renderer.addClass(document.body, 'modal-open');
this.appear.emit();
} else {
this.modalService.clearModal();
this.renderer.removeClass(document.body, 'modal-open');
this.disappear.emit();
this.destroy$.next();
}
}
ngOnDestroy(): void {
this.destroy$.next();
@ -141,9 +164,7 @@ export class ModalComponent implements OnDestroy {
debounceTime(150),
filter((key: KeyboardEvent) => key && key.key === 'Escape'),
)
.subscribe(() => {
this.close();
});
.subscribe(() => this.close());
fromEvent(window, 'beforeunload')
.pipe(takeUntil(this.destroy$))
@ -151,6 +172,7 @@ export class ModalComponent implements OnDestroy {
if (this.isFormDirty) {
event.returnValue = true;
} else {
event.returnValue = false;
delete event.returnValue;
}
});

@ -1,9 +1,13 @@
import { LocalizationPipe } from '@abp/ng.core';
import { RouterTestingModule } from '@angular/router/testing';
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store';
import { timer } from 'rxjs';
import { fromEvent, Subject, timer } from 'rxjs';
import { delay, reduce, take } from 'rxjs/operators';
import { ButtonComponent, ConfirmationComponent, ModalComponent } from '../components';
import { RouterTestingModule } from '@angular/router/testing';
import { ModalContainerComponent } from '../components/modal/modal-container.component';
import { Confirmation } from '../models';
import { ConfirmationService, ModalService } from '../services';
describe('ModalComponent', () => {
let spectator: SpectatorHost<
@ -12,16 +16,34 @@ describe('ModalComponent', () => {
>;
let appearFn;
let disappearFn;
let mockConfirmation$: Subject<Confirmation.Status>;
const createHost = createHostFactory({
component: ModalComponent,
imports: [RouterTestingModule],
declarations: [ConfirmationComponent, LocalizationPipe, ButtonComponent],
declarations: [
ConfirmationComponent,
LocalizationPipe,
ButtonComponent,
ModalContainerComponent,
],
entryComponents: [ModalContainerComponent],
providers: [
{
provide: ConfirmationService,
useValue: {
warn() {
mockConfirmation$ = new Subject();
return mockConfirmation$;
},
},
},
],
mocks: [Store],
});
beforeEach(() => {
appearFn = jest.fn(() => null);
disappearFn = jest.fn(() => null);
beforeEach(async () => {
appearFn = jest.fn();
disappearFn = jest.fn();
spectator = createHost(
`<abp-modal [(visible)]="visible" [busy]="busy" [centered]="true" (appear)="appearFn()" (disappear)="disappearFn()" size="sm" modalClass="test">
@ -30,102 +52,170 @@ describe('ModalComponent', () => {
</ng-template>
<ng-template #abpBody>
<div class="body">
<input [class.ng-dirty]="ngDirty">
</div>
<div class="body"><input [class.ng-dirty]="ngDirty"></div>
</ng-template>
<ng-template #abpFooter>
<div class="footer">
<button id="abp-close" #abpClose></button>
<div class="footer">
<button id="abp-close" #abpClose></button>
<abp-button>Submit</abp-button>
</div>
</ng-template>
<abp-button>Submit</abp-button>
<abp-confirmation></abp-confirmation>
</ng-template>
</abp-modal>
`,
{
hostProps: {
visible: true,
busy: false,
ngDirty: true,
ngDirty: false,
appearFn,
disappearFn,
},
},
);
await wait0ms();
});
afterEach(() => {
const modalService = spectator.get(ModalService);
modalService.clearModal();
});
it('should be created', () => {
expect(spectator.query('div.modal')).toBeTruthy();
expect(spectator.query('div.modal-backdrop')).toBeTruthy();
expect(spectator.query('div#abp-modal-dialog')).toBeTruthy();
it('should project its template to abp-modal-container', () => {
const modal = selectModal();
expect(modal).toBeTruthy();
expect(modal.querySelector('div.modal-backdrop')).toBeTruthy();
expect(modal.querySelector('div#abp-modal-dialog')).toBeTruthy();
});
it('should works right the inputs', () => {
expect(spectator.query('div.test')).toBeTruthy();
expect(spectator.query('div.modal-sm')).toBeTruthy();
expect(spectator.query('div.modal-dialog-centered')).toBeTruthy();
it('should reflect its input properties to the template', () => {
const modal = selectModal('.test');
expect(modal).toBeTruthy();
expect(modal.querySelector('div.modal-sm')).toBeTruthy();
expect(modal.querySelector('div.modal-dialog-centered')).toBeTruthy();
});
it('should emit the appear output', () => {
it('should emit the appear output when made visible', () => {
expect(appearFn).toHaveBeenCalled();
});
it('should emit the disappear output', () => {
it('should emit the disappear output when made invisible', async () => {
spectator.hostComponent.visible = false;
spectator.detectChanges();
expect(disappearFn).toHaveBeenCalled();
await wait0ms();
expect(disappearFn).toHaveBeenCalledTimes(1);
});
it('should open the confirmation popup and works correct', done => {
setTimeout(() => {
spectator.click('#abp-modal-close-button');
expect(disappearFn).not.toHaveBeenCalled();
expect(spectator.query('div.confirmation')).toBeTruthy();
spectator.click('button#cancel');
expect(spectator.query('div.modal')).toBeTruthy();
spectator.click('#abp-modal-close-button');
spectator.click('button#confirm');
expect(spectator.query('div.modal')).toBeFalsy();
expect(disappearFn).toHaveBeenCalled();
done();
}, 100);
xit('should close with the abpClose', async () => {
await wait0ms();
spectator.dispatchMouseEvent(spectator.component.abpClose, 'click');
await wait0ms();
expect(disappearFn).toHaveBeenCalledTimes(1);
});
it('should close with the abpClose', done => {
spectator.hostComponent.ngDirty = false;
it('should open the confirmation popup and works correct', async () => {
const confirmationService = spectator.get(ConfirmationService);
const warnSpy = jest.spyOn(confirmationService, 'warn');
await wait0ms();
spectator.hostComponent.ngDirty = true;
spectator.detectChanges();
setTimeout(() => {
spectator.click('#abp-close');
expect(disappearFn).toHaveBeenCalled();
done();
}, 10);
expect(selectModal()).toBeTruthy();
spectator.component.close(); // 1st try
await wait0ms();
spectator.component.close(); // 2nd try
await wait0ms();
expect(selectModal()).toBeTruthy();
expect(warnSpy).toHaveBeenCalledTimes(1);
warnSpy.mockClear();
mockConfirmation$.next(Confirmation.Status.reject);
await wait0ms();
expect(selectModal()).toBeTruthy();
spectator.component.close();
await wait0ms();
expect(selectModal()).toBeTruthy();
expect(warnSpy).toHaveBeenCalledTimes(1);
warnSpy.mockClear();
mockConfirmation$.next(Confirmation.Status.confirm);
await wait0ms();
expect(selectModal()).toBeNull();
expect(disappearFn).toHaveBeenCalledTimes(1);
});
it('should close with esc key', done => {
spectator.hostComponent.ngDirty = false;
it('should close with esc key', async () => {
await wait0ms();
spectator.dispatchKeyboardEvent(document.body, 'keyup', 'Escape');
await wait300ms();
expect(spectator.component.visible).toBe(false);
});
it('should not close when busy is true', async () => {
spectator.hostComponent.busy = true;
spectator.detectChanges();
timer(0).subscribe(() => {
spectator.dispatchKeyboardEvent(document.body, 'keyup', 'Escape');
});
timer(300).subscribe(() => {
expect(spectator.component.visible).toBe(false);
done();
});
spectator.component.close();
await wait0ms();
expect(disappearFn).not.toHaveBeenCalled();
});
it('should not close when busy is true', done => {
setTimeout(() => {
spectator.hostComponent.busy = true;
spectator.hostComponent.ngDirty = false;
spectator.detectChanges();
spectator.click('#abp-modal-close-button');
expect(disappearFn).not.toHaveBeenCalled();
expect(spectator.component.abpSubmit.loading).toBe(true);
done();
}, 0);
it('should not let window unload when form is dirty', async done => {
fromEvent(window, 'beforeunload')
.pipe(
take(2),
delay(0),
reduce<Event[]>((acc, v) => acc.concat(v), []),
)
.subscribe(([event1, event2]) => {
expect(event1.returnValue).toBe(true);
expect(event2.returnValue).toBe(false);
done();
});
spectator.hostComponent.ngDirty = true;
spectator.detectChanges();
spectator.dispatchFakeEvent(window, 'beforeunload');
await wait0ms();
spectator.hostComponent.ngDirty = false;
spectator.detectChanges();
spectator.dispatchFakeEvent(window, 'beforeunload');
});
});
function selectModal(modalSelector = ''): Element {
return document.querySelector(`abp-modal-container div.modal${modalSelector}`);
}
async function wait0ms() {
await timer(0).toPromise();
}
async function wait300ms() {
await timer(300).toPromise();
}

Loading…
Cancel
Save