diff --git a/components/core/style/index.less b/components/core/style/index.less
index 7c26ea7b48f..27acc8f5bfc 100644
--- a/components/core/style/index.less
+++ b/components/core/style/index.less
@@ -26,7 +26,7 @@
.cdk-overlay-pane {
position: absolute;
pointer-events: auto;
- z-index: 1000;
+ // z-index: 1000; // Give an opportunity to the content own to manage their z-index such as Modal
}
.box-shadow-left() {
diff --git a/components/modal/demo/basic.ts b/components/modal/demo/basic.ts
index e0a1be5a128..6762b6c1b6f 100644
--- a/components/modal/demo/basic.ts
+++ b/components/modal/demo/basic.ts
@@ -1,10 +1,11 @@
-import { Component } from '@angular/core';
+import { Component, ViewChild, OnInit } from '@angular/core';
+import { NzModalComponent } from 'ng-zorro-antd';
@Component({
selector: 'nz-demo-modal-basic',
template: `
-
+
Content one
Content two
Content three
@@ -12,13 +13,21 @@ import { Component } from '@angular/core';
`,
styles: []
})
-export class NzDemoModalBasicComponent {
+export class NzDemoModalBasicComponent implements OnInit {
isVisible = false;
+ modalValid = true;
+
+ @ViewChild('modal') private modal: NzModalComponent;
constructor() {}
+ ngOnInit(): void {
+ (window as any).modal = this.modal; // tslint:disable-line
+ }
+
showModal(): void {
this.isVisible = true;
+ window.setTimeout(() => this.modalValid = false, 2000);
}
handleOk(): void {
diff --git a/components/modal/demo/service.ts b/components/modal/demo/service.ts
index e1997328464..c26d8d9bdf4 100644
--- a/components/modal/demo/service.ts
+++ b/components/modal/demo/service.ts
@@ -1,6 +1,6 @@
/* entryComponents: NzModalCustomComponent */
-import { Component, Input, TemplateRef } from '@angular/core';
+import { Component, Input, TemplateRef, ViewChild } from '@angular/core';
import { NzModalRef, NzModalService } from 'ng-zorro-antd';
@Component({
@@ -34,11 +34,17 @@ import { NzModalRef, NzModalService } from 'ng-zorro-antd';
+
+
+
+
+ This is a non-service html modal
`
})
export class NzDemoModalServiceComponent {
tplModal: NzModalRef;
tplModalButtonLoading = false;
+ htmlModalVisible = false;
constructor(private modalService: NzModalService) { }
@@ -86,8 +92,10 @@ export class NzDemoModalServiceComponent {
}]
});
+ modal.afterOpen.subscribe(() => console.log('[afterOpen] emitted!'));
+
// Return a result when closed
- modal.afterClose().subscribe((result) => console.log('[afterClose] The result is:', result));
+ modal.afterClose.subscribe((result) => console.log('[afterClose] The result is:', result));
// delay until modal instance created
window.setTimeout(() => {
@@ -133,6 +141,23 @@ export class NzDemoModalServiceComponent {
]
});
}
+
+ openAndCloseAll(): void {
+ let pos = 0;
+
+ [ 'create', 'info', 'success', 'error' ].forEach((method) => this.modalService[method]({
+ nzMask: false,
+ nzTitle: `Test ${method} title`,
+ nzContent: `Test content: ${method}`,
+ nzStyle: { position: 'absolute', top: `${pos * 70}px`, left: `${(pos++) * 300}px` }
+ }));
+
+ this.htmlModalVisible = true;
+
+ this.modalService.afterAllClose.subscribe(() => console.log('afterAllClose emitted!'));
+
+ window.setTimeout(() => this.modalService.closeAll(), 2000);
+ }
}
@Component({
diff --git a/components/modal/doc/index.en-US.md b/components/modal/doc/index.en-US.md
index a5a3316c7fa..d70b2c6970f 100644
--- a/components/modal/doc/index.en-US.md
+++ b/components/modal/doc/index.en-US.md
@@ -23,7 +23,8 @@ The dialog is currently divided into 2 modes, `normal mode` and `confirm box mod
| Property | Description | Type | Default |
|----|----|----|----|
-| nzAfterClose | Specify a EventEmitter that will be emitted when modal is closed completely. | EventEmitter | - |
+| nzAfterOpen | Specify a EventEmitter that will be emitted when modal opened | EventEmitter | - |
+| nzAfterClose | Specify a EventEmitter that will be emitted when modal is closed completely (Can listen for parameters passed in the close/destroy method) | EventEmitter | - |
| nzBodyStyle | Body style for modal body element. Such as height, padding etc. | object | - |
| nzCancelText | Text of the Cancel button. Set to null to show no cancel button (this value is invalid if the nzFooter parameter is used in normal mode) | string | Cancel |
| nzClosable | Whether a close (x) button is visible on top right of the modal dialog or not. Invalid value in confirm box mode (default will be hidden) | boolean | true |
@@ -81,22 +82,25 @@ All the `NzModalService.method`s will return a reference, and then we can close
```ts
constructor(modal: NzModalService) {
const ref: NzModalRef = modal.info();
- ref.destroy(); // Note: This dialog will be destroyed directly
+ ref.close(); // Or ref.destroy(); This dialog will be destroyed directly
}
```
### Related type definition
-#### NzModalRef (used for control dialogs)
+#### NzModalRef
+
+> NzModalRef object is used to control dialogs and communicate with inside content
The dialog created by the service method `NzModalService.xxx()` will return a `NzModalRef` object that is used to manipulate the dialog (this object can also be obtained by dependency injection `NzModalRef` if `nzContent` is used as Component) , This object has the following methods:
| Method | Description |
|----|----|
+| afterOpen | Same as nzAfterOpen but of type Observable<void> |
+| afterClose | Same as nzAfterClose, but of type Observable<result:any> |
| open() | Open (display) dialog box. Calling this function will fail if the dialog is already destroyed |
| close() | Close (hide) the dialog. Note: When used for a dialog created as a service, this method will destroy the dialog directly (as with the destroy method) |
| destroy() | Destroy the dialog. Note: Used only for dialogs created by the service (non-service created dialogs, this method only hides the dialog) |
-| afterClose() | Returns an Observable object to get the result parameter passed in close/destroy (will fire after the dialog is closed) |
| getContentComponent() | Gets the Component instance in the contents of the dialog for `nzContent`. Note: When the dialog is not initialized (`ngOnInit` is not executed), this function will return `undefined` |
#### ModalButtonOptions (used to customize the bottom button)
diff --git a/components/modal/doc/index.zh-CN.md b/components/modal/doc/index.zh-CN.md
index 8eaf0b65694..a8068a701a6 100644
--- a/components/modal/doc/index.zh-CN.md
+++ b/components/modal/doc/index.zh-CN.md
@@ -23,7 +23,8 @@ title: Modal
| 参数 | 说明 | 类型 | 默认值 |
|----|----|----|----|
-| nzAfterClose | Modal 完全关闭后的回调 | EventEmitter | 无 |
+| nzAfterOpen | Modal 打开后的回调 | EventEmitter | 无 |
+| nzAfterClose | Modal 完全关闭后的回调,可监听close/destroy方法传入的参数 | EventEmitter | 无 |
| nzBodyStyle | Modal body 样式 | object | 无 |
| nzCancelText | 取消按钮文字。设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效) | string | 取消 |
| nzClosable | 是否显示右上角的关闭按钮。确认框模式下该值无效(默认会被隐藏) | boolean | true |
@@ -80,22 +81,33 @@ title: Modal
```ts
constructor(modal: NzModalService) {
const ref: NzModalRef = modal.info();
- ref.destroy(); // 注:这里将直接销毁对话框
+ ref.close(); // 或 ref.destroy(); 将直接销毁对话框
}
```
### 相关类型定义
-#### NzModalRef(用于控制对话框)
+#### NzModalService的其他方法/属性
+
+| 方法/属性 | 说明 | 类型 |
+|----|----|
+| openModals | 当前打开的所有Modal引用列表 | NzModalRef[] |
+| afterAllClose | 所有Modal完全关闭后的回调 | Observable<void> |
+| closeAll() | 关闭所有模态框 | function |
+
+#### NzModalRef
+
+> NzModalRef 对象用于控制对话框以及进行内容间的通信
通过服务方式 `NzModalService.xxx()` 创建的对话框,都会返回一个 `NzModalRef` 对象,用于操控该对话框(若使用nzContent为Component时,也可通过依赖注入 `NzModalRef` 方式获得此对象),该对象具有以下方法:
-| 方法 | 说明 |
+| 方法/属性 | 说明 |
|----|----|
+| afterOpen | 同nzAfterOpen,但类型为Observable<void> |
+| afterClose | 同nzAfterClose,但类型为Observable<result:any> |
| open() | 打开(显示)对话框。若对话框已销毁,则调用此函数将失效 |
| close(result: any) | 关闭(隐藏)对话框。注:当用于以服务方式创建的对话框,此方法将直接 销毁 对话框(同destroy方法) |
| destroy(result: any) | 销毁对话框。注:仅用于服务方式创建的对话框(非服务方式创建的对话框,此方法只会隐藏对话框) |
-| afterClose() | 返回一个Observable对象来获取close/destroy中传递的result参数(将在对话框关闭后触发) |
| getContentComponent() | 获取对话框内容中`nzContent`的Component实例instance。注:当对话框还未初始化完毕(`ngOnInit`未执行)时,此函数将返回`undefined` |
#### ModalButtonOptions(用于自定义底部按钮)
diff --git a/components/modal/nz-modal-control.service.ts b/components/modal/nz-modal-control.service.ts
new file mode 100644
index 00000000000..6bfeb3a44b6
--- /dev/null
+++ b/components/modal/nz-modal-control.service.ts
@@ -0,0 +1,72 @@
+import { Injectable, Optional, SkipSelf } from '@angular/core';
+import { Subject } from 'rxjs/Subject';
+import { Subscription } from 'rxjs/Subscription';
+
+import { NzModalRef } from './nz-modal-ref.class';
+
+interface RegisteredMeta {
+ modalRef: NzModalRef;
+ afterOpenSubscription: Subscription;
+ afterCloseSubscription: Subscription;
+}
+
+@Injectable()
+export class NzModalControlService {
+ // Track singleton afterAllClose through over the injection tree
+ get afterAllClose(): Subject {
+ return this.parentService ? this.parentService.afterAllClose : this.rootAfterAllClose;
+ }
+ // Track singleton openModals array through over the injection tree
+ get openModals(): NzModalRef[] {
+ return this.parentService ? this.parentService.openModals : this.rootOpenModals;
+ }
+
+ private rootOpenModals: NzModalRef[] = this.parentService ? null : [];
+ private rootAfterAllClose: Subject = this.parentService ? null : new Subject();
+
+ private rootRegisteredMetaMap: Map = this.parentService ? null : new Map();
+ private get registeredMetaMap(): Map { // Registered modal for later usage
+ return this.parentService ? this.parentService.registeredMetaMap : this.rootRegisteredMetaMap;
+ }
+
+ constructor(
+ @Optional() @SkipSelf() private parentService: NzModalControlService) {}
+
+ // Register a modal to listen its open/close
+ registerModal(modalRef: NzModalRef): void {
+ if (!this.hasRegistered(modalRef)) {
+ const afterOpenSubscription = modalRef.afterOpen.subscribe(() => this.openModals.push(modalRef));
+ const afterCloseSubscription = modalRef.afterClose.subscribe(() => this.removeOpenModal(modalRef));
+
+ this.registeredMetaMap.set(modalRef, { modalRef, afterOpenSubscription, afterCloseSubscription });
+ }
+ }
+
+ // TODO: allow deregister modals
+ // deregisterModal(modalRef: NzModalRef): void {}
+
+ hasRegistered(modalRef: NzModalRef): boolean {
+ return this.registeredMetaMap.has(modalRef);
+ }
+
+ // Close all registered opened modals
+ closeAll(): void {
+ let i = this.openModals.length;
+
+ while (i--) {
+ this.openModals[i].close();
+ }
+ }
+
+ private removeOpenModal(modalRef: NzModalRef): void {
+ const index = this.openModals.indexOf(modalRef);
+
+ if (index > -1) {
+ this.openModals.splice(index, 1);
+
+ if (!this.openModals.length) {
+ this.afterAllClose.next();
+ }
+ }
+ }
+}
diff --git a/components/modal/nz-modal-ref.class.ts b/components/modal/nz-modal-ref.class.ts
index 632c124efed..fd5f0815e79 100644
--- a/components/modal/nz-modal-ref.class.ts
+++ b/components/modal/nz-modal-ref.class.ts
@@ -8,10 +8,12 @@ import { NzModalComponent } from './nz-modal.component';
* NzModalRef is aim to avoid accessing to the modal instance directly by users.
*/
export abstract class NzModalRef { // tslint:disable-line:no-any
+ abstract afterOpen: Observable;
+ abstract afterClose: Observable;
+
abstract open(): void;
abstract close(result?: R): void;
abstract destroy(result?: R): void;
- abstract afterClose(): Observable;
// /**
// * Return the ComponentRef of nzContent when specify nzContent as a Component
diff --git a/components/modal/nz-modal.component.ts b/components/modal/nz-modal.component.ts
index 5a8f4eba4de..cb8b01ea381 100644
--- a/components/modal/nz-modal.component.ts
+++ b/components/modal/nz-modal.component.ts
@@ -1,4 +1,4 @@
-import { Overlay } from '@angular/cdk/overlay';
+import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { DOCUMENT } from '@angular/common';
import {
AfterViewInit,
@@ -11,6 +11,7 @@ import {
Injector,
Input,
OnChanges,
+ OnDestroy,
OnInit,
Output,
Renderer2,
@@ -22,10 +23,12 @@ import {
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
+import { toBoolean } from '../core/util/convert';
import { measureScrollbar } from '../core/util/mesure-scrollbar';
import { NzI18nService } from '../i18n/nz-i18n.service';
import ModalUtil from './modal-util';
+import { NzModalControlService } from './nz-modal-control.service';
import { NzModalRef } from './nz-modal-ref.class';
import { ModalButtonOptions, ModalOptions, ModalType, OnClickCallback } from './nz-modal.type';
@@ -38,20 +41,25 @@ interface ClassMap {
type AnimationState = 'enter' | 'leave' | null;
@Component({
- selector : 'nz-modal',
+ selector: 'nz-modal',
templateUrl: './nz-modal.component.html'
})
// tslint:disable-next-line:no-any
-export class NzModalComponent extends NzModalRef implements OnInit, OnChanges, AfterViewInit, ModalOptions {
+export class NzModalComponent extends NzModalRef implements OnInit, OnChanges, AfterViewInit, OnDestroy, ModalOptions {
@Input() nzModalType: ModalType = 'default';
@Input() nzContent: string | TemplateRef<{}> | Type; // [STATIC] If not specified, will use
@Input() nzComponentParams: object; // [STATIC] ONLY avaliable when nzContent is a component
@Input() nzFooter: string | TemplateRef<{}> | Array>; // [STATIC] Default Modal ONLY
- @Input() nzGetContainer: HTMLElement | (() => HTMLElement) = () => this.overlay.create().overlayElement; // [STATIC]
+ @Input() nzGetContainer: HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) = () => this.overlay.create(); // [STATIC]
+
+ @Input()
+ get nzVisible(): boolean { return this._visible; }
+ set nzVisible(value: boolean) { this._visible = toBoolean(value); }
+ private _visible: boolean = false;
- @Input() nzVisible = false;
@Output() nzVisibleChange = new EventEmitter();
+
@Input() nzZIndex: number = 1000;
@Input() nzWidth: number | string = 520;
@Input() nzWrapClassName: string;
@@ -59,22 +67,53 @@ export class NzModalComponent extends NzModalRef impleme
@Input() nzStyle: object;
@Input() nzIconType: string = 'question-circle'; // Confirm Modal ONLY
@Input() nzTitle: string | TemplateRef<{}>;
- @Input() nzClosable = true;
- @Input() nzMask = true;
- @Input() nzMaskClosable = true;
+
+ @Input()
+ get nzClosable(): boolean { return this._closable; }
+ set nzClosable(value: boolean) { this._closable = toBoolean(value); }
+ private _closable: boolean = true;
+
+ @Input()
+ get nzMask(): boolean { return this._mask; }
+ set nzMask(value: boolean) { this._mask = toBoolean(value); }
+ private _mask: boolean = true;
+
+ @Input()
+ get nzMaskClosable(): boolean { return this._maskClosable; }
+ set nzMaskClosable(value: boolean) { this._maskClosable = toBoolean(value); }
+ private _maskClosable: boolean = true;
+
@Input() nzMaskStyle: object;
@Input() nzBodyStyle: object;
- @Output() nzAfterClose = new EventEmitter(); // Trigger when modal is hidden
+
+ @Output() nzAfterOpen = new EventEmitter(); // Trigger when modal open(visible) after animations
+ @Output() nzAfterClose = new EventEmitter(); // Trigger when modal leave-animation over
+ get afterOpen(): Observable { // Observable alias for nzAfterOpen
+ return this.nzAfterOpen.asObservable();
+ }
+ get afterClose(): Observable { // Observable alias for nzAfterClose
+ return this.nzAfterClose.asObservable();
+ }
// --- Predefined OK & Cancel buttons
@Input() nzOkText: string;
@Input() nzOkType = 'primary';
- @Input() nzOkLoading = false;
- @Input() @Output() nzOnOk: EventEmitter | OnClickCallback = new EventEmitter();
+
+ @Input()
+ get nzOkLoading(): boolean { return this._okLoading; }
+ set nzOkLoading(value: boolean) { this._okLoading = toBoolean(value); }
+ private _okLoading: boolean = false;
+
+ @Input() @Output() nzOnOk: EventEmitter | OnClickCallback = new EventEmitter();
@ViewChild('autoFocusButtonOk', { read: ElementRef }) autoFocusButtonOk: ElementRef; // Only aim to focus the ok button that needs to be auto focused
@Input() nzCancelText: string;
- @Input() nzCancelLoading = false;
- @Input() @Output() nzOnCancel: EventEmitter | OnClickCallback = new EventEmitter();
+
+ @Input()
+ get nzCancelLoading(): boolean { return this._cancelLoading; }
+ set nzCancelLoading(value: boolean) { this._cancelLoading = toBoolean(value); }
+ private _cancelLoading: boolean = false;
+
+ @Input() @Output() nzOnCancel: EventEmitter | OnClickCallback = new EventEmitter();
@ViewChild('modalContainer') modalContainer: ElementRef;
@ViewChild('bodyContainer', { read: ViewContainerRef }) bodyContainer: ViewContainerRef;
@@ -88,15 +127,18 @@ export class NzModalComponent extends NzModalRef impleme
private contentComponentRef: ComponentRef; // Handle the reference when using nzContent as Component
private animationState: AnimationState; // Current animation state
+ private container: HTMLElement | OverlayRef;
+
+ constructor(
+ private overlay: Overlay,
+ private locale: NzI18nService,
+ private renderer: Renderer2,
+ private cfr: ComponentFactoryResolver,
+ private elementRef: ElementRef,
+ private viewContainer: ViewContainerRef,
+ private modalControl: NzModalControlService,
+ @Inject(DOCUMENT) private document: any) { // tslint:disable-line:no-any
- constructor(private overlay: Overlay,
- private locale: NzI18nService,
- private renderer: Renderer2,
- private cfr: ComponentFactoryResolver,
- private elementRef: ElementRef,
- private viewContainer: ViewContainerRef,
- @Inject(DOCUMENT) private document: any // tslint:disable-line:no-any
- ) {
super();
}
@@ -109,10 +151,16 @@ export class NzModalComponent extends NzModalRef impleme
this.nzFooter = this.formatModalButtons(this.nzFooter as Array>);
}
- const container = typeof this.nzGetContainer === 'function' ? this.nzGetContainer() : this.nzGetContainer;
- if (container instanceof HTMLElement) {
- container.appendChild(this.elementRef.nativeElement);
+ // Place the modal dom to elsewhere
+ this.container = typeof this.nzGetContainer === 'function' ? this.nzGetContainer() : this.nzGetContainer;
+ if (this.container instanceof HTMLElement) {
+ this.container.appendChild(this.elementRef.nativeElement);
+ } else if (this.container instanceof OverlayRef) { // NOTE: only attach the dom to overlay, the view container is not changed actually
+ this.container.overlayElement.appendChild(this.elementRef.nativeElement);
}
+
+ // Register modal when afterOpen/afterClose is stable
+ this.modalControl.registerModal(this);
}
// [NOTE] NOT available when using by service!
@@ -121,10 +169,7 @@ export class NzModalComponent extends NzModalRef impleme
// BUT: User also can change "nzContent" dynamically to trigger UI changes (provided you don't use Component that needs initializations)
ngOnChanges(changes: SimpleChanges): void {
if (changes.nzVisible) {
- this.changeBodyOverflow(this.nzVisible);
- if (!changes.nzVisible.firstChange) { // Do not trigger animation while initializing
- this.animateTo(this.nzVisible);
- }
+ this.handleVisibleStateChange(this.nzVisible, !changes.nzVisible.firstChange); // Do not trigger animation while initializing
}
}
@@ -139,22 +184,24 @@ export class NzModalComponent extends NzModalRef impleme
}
}
+ ngOnDestroy(): void {
+ if (this.container instanceof OverlayRef) {
+ this.container.dispose();
+ }
+ }
+
open(): void {
this.changeVisibleFromInside(true);
}
close(result?: R): void {
- this.changeVisibleFromInside(false).then(() => this.nzAfterClose.emit(result));
+ this.changeVisibleFromInside(false, result);
}
destroy(result?: R): void { // Destroy equals Close
this.close(result);
}
- afterClose(): Observable {
- return this.nzAfterClose.asObservable();
- }
-
getInstance(): NzModalComponent {
return this;
}
@@ -222,6 +269,20 @@ export class NzModalComponent extends NzModalRef impleme
return Array.isArray(value) && value.length > 0;
}
+ // Do rest things when visible state changed
+ private handleVisibleStateChange(visible: boolean, animation: boolean = true, closeResult?: R): Promise {
+ return Promise
+ .resolve(animation && this.animateTo(visible))
+ .then(() => { // Emit open/close event after animations over
+ if (visible) {
+ this.nzAfterOpen.emit();
+ } else {
+ this.nzAfterClose.emit(closeResult);
+ }
+ })
+ .then(() => this.changeBodyOverflow());
+ }
+
// Lookup a button's property, if the prop is a function, call & then return the result, otherwise, return itself.
private getButtonCallableProp(options: ModalButtonOptions, prop: string): {} {
const value = options[ prop ];
@@ -242,13 +303,12 @@ export class NzModalComponent extends NzModalRef impleme
}
// Change nzVisible from inside
- private changeVisibleFromInside(visible: boolean): Promise {
+ private changeVisibleFromInside(visible: boolean, closeResult?: R): Promise {
if (this.nzVisible !== visible) {
// Change nzVisible value immediately
this.nzVisible = visible;
- this.changeBodyOverflow(this.nzVisible);
this.nzVisibleChange.emit(visible);
- return this.animateTo(visible);
+ return this.handleVisibleStateChange(visible, true, closeResult);
}
return Promise.resolve();
}
@@ -331,23 +391,16 @@ export class NzModalComponent extends NzModalRef impleme
// }
}
- private changeBodyOverflow(visible: boolean): void {
- const countKey = 'data-modal-count';
- let countValue = parseInt(this.document.body.attributes.getNamedItem(countKey) && this.document.body.attributes.getNamedItem('data-modal-count').value || 0, 10);
- if (visible) {
- countValue += 1;
- } else {
- countValue = (countValue - 1 >= 0) ? (countValue - 1) : 0;
- }
- if (countValue) {
- const scrollBarWidth = measureScrollbar();
- this.renderer.setStyle(this.document.body, 'padding-right', `${scrollBarWidth}px`);
+ private changeBodyOverflow(): void {
+ const openModals = this.modalControl.openModals;
+
+ if (openModals.length) {
+ this.renderer.setStyle(this.document.body, 'padding-right', `${measureScrollbar()}px`);
this.renderer.setStyle(this.document.body, 'overflow', 'hidden');
} else {
this.renderer.removeStyle(this.document.body, 'padding-right');
this.renderer.removeStyle(this.document.body, 'overflow');
}
- this.renderer.setAttribute(this.document.body, countKey, `${countValue}`);
}
}
diff --git a/components/modal/nz-modal.module.ts b/components/modal/nz-modal.module.ts
index d7164eb7497..03c990dd6c1 100644
--- a/components/modal/nz-modal.module.ts
+++ b/components/modal/nz-modal.module.ts
@@ -7,6 +7,7 @@ import { LoggerModule } from '../core/util/logger/logger.module';
import { NzI18nModule } from '../i18n/nz-i18n.module';
import { CssUnitPipe } from './css-unit.pipe';
+import { NzModalControlService } from './nz-modal-control.service';
import { NzModalComponent } from './nz-modal.component';
import { NzModalService } from './nz-modal.service';
@@ -15,6 +16,6 @@ import { NzModalService } from './nz-modal.service';
exports: [ NzModalComponent ],
declarations: [ NzModalComponent, CssUnitPipe ],
entryComponents: [ NzModalComponent ],
- providers: [ NzModalService ]
+ providers: [ NzModalControlService, NzModalService ]
})
export class NzModalModule { }
diff --git a/components/modal/nz-modal.service.ts b/components/modal/nz-modal.service.ts
index 7a57d56797a..1e54338578f 100644
--- a/components/modal/nz-modal.service.ts
+++ b/components/modal/nz-modal.service.ts
@@ -1,9 +1,11 @@
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
-import { ApplicationRef, ComponentFactoryResolver, ComponentRef, EventEmitter, Injectable, Injector, TemplateRef, Type } from '@angular/core';
+import { ApplicationRef, ComponentFactoryResolver, ComponentRef, EventEmitter, Injectable, Injector, Optional, SkipSelf, TemplateRef, Type } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
import { LoggerService } from '../core/util/logger/logger.service';
+import { NzModalControlService } from './nz-modal-control.service';
import { NzModalRef } from './nz-modal-ref.class';
import { NzModalComponent } from './nz-modal.component';
import { ConfirmType, ModalOptions, ModalOptionsForService } from './nz-modal.type';
@@ -14,15 +16,15 @@ export class ModalBuilderForService {
private overlayRef: OverlayRef;
constructor(private overlay: Overlay, options: ModalOptionsForService = {}) {
- this.createModal();
+ this.createModal();
- if (!('nzGetContainer' in options)) { // As we use CDK to create modal in service by force, there is no need to use nzGetContainer
- options.nzGetContainer = null; // Override nzGetContainer's default value to prevent creating another overlay
- }
+ if (!('nzGetContainer' in options)) { // As we use CDK to create modal in service by force, there is no need to use nzGetContainer
+ options.nzGetContainer = null; // Override nzGetContainer's default value to prevent creating another overlay
+ }
- this.changeProps(options);
- this.modalRef.instance.open();
- this.modalRef.instance.nzAfterClose.subscribe(() => this.destroyModal()); // [NOTE] By default, close equals destroy when using as Service
+ this.changeProps(options);
+ this.modalRef.instance.open();
+ this.modalRef.instance.nzAfterClose.subscribe(() => this.destroyModal()); // [NOTE] By default, close equals destroy when using as Service
}
getInstance(): NzModalComponent {
@@ -51,15 +53,33 @@ export class ModalBuilderForService {
@Injectable()
export class NzModalService {
+ // Track of the current close modals (we assume invisible is close this time)
+ get openModals(): NzModalRef[] {
+ return this.modalControl.openModals;
+ }
+
+ get afterAllClose(): Observable {
+ return this.modalControl.afterAllClose.asObservable();
+ }
- constructor(private overlay: Overlay, private logger: LoggerService) { }
+ constructor(
+ private overlay: Overlay,
+ private logger: LoggerService,
+ private modalControl: NzModalControlService) { }
+
+ // Closes all of the currently-open dialogs
+ closeAll(): void {
+ this.modalControl.closeAll();
+ }
create(options: ModalOptionsForService = {}): NzModalRef {
if (typeof options.nzOnCancel !== 'function') {
options.nzOnCancel = () => {}; // Leave a empty function to close this modal by default
}
- return new ModalBuilderForService(this.overlay, options).getInstance();
+ const modalRef = new ModalBuilderForService(this.overlay, options).getInstance(); // NOTE: use NzModalComponent as the NzModalRef by now, we may need archive the real NzModalRef object in the future
+
+ return modalRef;
}
confirm(options: ModalOptionsForService = {}, confirmType: ConfirmType = 'confirm'): NzModalRef {
diff --git a/components/modal/nz-modal.spec.ts b/components/modal/nz-modal.spec.ts
index 9b7c5794cd9..0ede87ee160 100644
--- a/components/modal/nz-modal.spec.ts
+++ b/components/modal/nz-modal.spec.ts
@@ -1,12 +1,16 @@
-import { Component, DebugElement, ElementRef, Input, NgModule, OnInit } from '@angular/core';
-import { async, fakeAsync, flush, tick, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
+/* TODO: Sort out and rewrite for more standardized */
+
+import { Component, DebugElement, ElementRef, EventEmitter, Input, NgModule } from '@angular/core';
+import { async, fakeAsync, flush, flushMicrotasks, inject, tick, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
+import { OverlayContainer } from '@angular/cdk/overlay';
import { NzButtonComponent } from '../button/nz-button.component';
import { NzButtonModule } from '../button/nz-button.module';
import { CssUnitPipe } from './css-unit.pipe';
+import { NzModalControlService } from './nz-modal-control.service';
import { NzModalRef } from './nz-modal-ref.class';
import { MODAL_ANIMATE_DURATION, NzModalComponent } from './nz-modal.component';
import { NzModalModule } from './nz-modal.module';
@@ -14,7 +18,7 @@ import { NzModalService } from './nz-modal.service';
const WAIT_ANIMATE_TIME = MODAL_ANIMATE_DURATION + 50;
-describe('modal', () => {
+describe('modal testing (legacy)', () => {
let instance;
let fixture: ComponentFixture<{}>;
@@ -194,6 +198,8 @@ describe('modal', () => {
fixture.detectChanges(); // Initial change detecting
const contentComponent = modalAgent.getContentComponent();
+ const contentComponentRef = (modalAgent as any).getContentComponentRef(); // tslint:disable-line:no-any
+ expect(contentComponent).toBe(contentComponentRef.instance);
const contentElement = contentComponent.elementRef.nativeElement as HTMLElement;
// change title from outside
const firstButton = modalElement.querySelector('.ant-modal-footer button:first-child') as HTMLButtonElement;
@@ -285,6 +291,143 @@ describe('modal', () => {
});
});
+describe('NzModal', () => {
+ let modalService: NzModalService;
+ let overlayContainer: OverlayContainer;
+ let overlayContainerElement: HTMLElement;
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [ NzModalModule ],
+ declarations: [
+ ModalByServiceComponent
+ ]
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ beforeEach(inject([ NzModalService, OverlayContainer ], (ms: NzModalService, oc: OverlayContainer) => {
+ modalService = ms;
+ overlayContainer = oc;
+ overlayContainerElement = oc.getContainerElement();
+ }));
+
+ afterEach(() => {
+ overlayContainer.ngOnDestroy();
+ });
+
+ describe('created by service', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ModalByServiceComponent);
+ });
+ afterEach(fakeAsync(() => { // wait all openModals tobe closed to clean up the ModalManager as it is globally static
+ modalService.closeAll();
+ fixture.detectChanges();
+ tick(1000);
+ }));
+
+ it('should trigger both afterOpen/nzAfterOpen and have the correct openModals length', fakeAsync(() => {
+ const spy = jasmine.createSpy('afterOpen spy');
+ const nzAfterOpen = new EventEmitter();
+ const modalRef = modalService.create({ nzAfterOpen });
+
+ modalRef.afterOpen.subscribe(spy);
+ nzAfterOpen.subscribe(spy);
+
+ fixture.detectChanges();
+ expect(spy).not.toHaveBeenCalled();
+
+ tick(600);
+ expect(spy).toHaveBeenCalledTimes(2);
+ expect(modalService.openModals.indexOf(modalRef)).toBeGreaterThan(-1);
+ expect(modalService.openModals.length).toBe(1);
+ }));
+
+ it('should trigger both afterClose/nzAfterClose and have the correct openModals length', fakeAsync(() => {
+ const spy = jasmine.createSpy('afterClose spy');
+ const nzAfterClose = new EventEmitter();
+ const modalRef = modalService.create({ nzAfterClose });
+
+ modalRef.afterClose.subscribe(spy);
+ nzAfterClose.subscribe(spy);
+
+ fixture.detectChanges();
+ tick(600);
+ modalRef.close();
+ fixture.detectChanges();
+ expect(spy).not.toHaveBeenCalled();
+
+ tick(600);
+ expect(spy).toHaveBeenCalledTimes(2);
+ expect(modalService.openModals.indexOf(modalRef)).toBe(-1);
+ expect(modalService.openModals.length).toBe(0);
+ }));
+
+ it('should return/receive with/without result data', fakeAsync(() => {
+ const spy = jasmine.createSpy('afterClose without result spy');
+ const modalRef = modalService.success();
+
+ modalRef.afterClose.subscribe(spy);
+ fixture.detectChanges();
+ tick(600);
+ modalRef.destroy();
+ expect(spy).not.toHaveBeenCalled();
+ tick(600);
+ expect(spy).toHaveBeenCalledWith(undefined);
+ }));
+
+ it('should return/receive with result data', fakeAsync(() => {
+ const result = { data: 'Fake Error' };
+ const spy = jasmine.createSpy('afterClose with result spy');
+ const modalRef = modalService.error();
+
+ fixture.detectChanges();
+ tick(600);
+ modalRef.destroy(result);
+ modalRef.afterClose.subscribe(spy);
+ expect(spy).not.toHaveBeenCalled();
+ tick(600);
+ expect(spy).toHaveBeenCalledWith(result);
+ }));
+
+ it('should close all opened modals (include non-service modals)', fakeAsync(() => {
+ const spy = jasmine.createSpy('afterAllClose spy');
+ const modalMethods = [ 'create', 'info', 'success', 'error', 'confirm' ];
+ const uniqueId = (name: string) => `__${name}_ID_SUFFIX__`;
+ const queryOverlayElement = (name: string) => overlayContainerElement.querySelector(`.${uniqueId(name)}`) as HTMLElement;
+
+ modalService.afterAllClose.subscribe(spy);
+
+ fixture.componentInstance.nonServiceModalVisible = true; // Show non-service modal
+ modalMethods.forEach(method => modalService[method]({ nzWrapClassName: uniqueId(method) })); // Service modals
+
+ fixture.detectChanges();
+ tick(600);
+ (modalMethods.concat('NON_SERVICE')).forEach(method => expect(queryOverlayElement(method).style.display).not.toBe('none')); // Cover non-service modal for later checking
+ expect(modalService.openModals.length).toBe(6);
+
+ modalService.closeAll();
+ fixture.detectChanges();
+ expect(spy).not.toHaveBeenCalled();
+ tick(600);
+ expect(spy).toHaveBeenCalled();
+ expect(modalService.openModals.length).toBe(0);
+ }));
+
+ it('should modal not be registered twice', fakeAsync(() => {
+ const modalRef = modalService.create();
+
+ fixture.detectChanges();
+ (modalService as any).modalControl.registerModal(modalRef); // tslint:disable-line:no-any
+ tick(600);
+ expect(modalService.openModals.length).toBe(1);
+ }));
+ });
+});
+
// -------------------------------------------
// | Testing Components
// -------------------------------------------
@@ -458,6 +601,19 @@ export class TestConfirmModalComponent {
})
class TestCssUnitPipeComponent { }
+@Component({
+ selector: 'nz-modal-by-service',
+ template: `
+
+ `,
+ providers: [ NzModalControlService ] // Testing for service with parent service
+})
+export class ModalByServiceComponent {
+ nonServiceModalVisible = false;
+
+ constructor(modalControlService: NzModalControlService) {}
+}
+
// -------------------------------------------
// | Local tool functions
// -------------------------------------------
diff --git a/components/modal/nz-modal.type.ts b/components/modal/nz-modal.type.ts
index 080ff0821e3..5d4c72b270c 100644
--- a/components/modal/nz-modal.type.ts
+++ b/components/modal/nz-modal.type.ts
@@ -1,3 +1,4 @@
+import { OverlayRef } from '@angular/cdk/overlay';
import { EventEmitter, TemplateRef, Type } from '@angular/core';
export type OnClickCallback = ((instance: T) => (false | void | {}) | Promise);
@@ -25,17 +26,18 @@ export interface ModalOptions { // tslint:disable-line:no-any
nzMaskStyle?: object;
nzBodyStyle?: object;
nzFooter?: string | TemplateRef<{}> | Array>; // Default Modal ONLY
- nzGetContainer?: HTMLElement | (() => HTMLElement); // STATIC
- nzAfterClose?: EventEmitter;
+ nzGetContainer?: HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef); // STATIC
+ nzAfterOpen?: EventEmitter;
+ nzAfterClose?: EventEmitter;
// --- Predefined OK & Cancel buttons
nzOkText?: string;
nzOkType?: string;
nzOkLoading?: boolean;
- nzOnOk?: EventEmitter | OnClickCallback; // Mixed using ng's Input/Output (Should care of "this" when using OnClickCallback)
+ nzOnOk?: EventEmitter | OnClickCallback; // Mixed using ng's Input/Output (Should care of "this" when using OnClickCallback)
nzCancelText?: string;
nzCancelLoading?: boolean;
- nzOnCancel?: EventEmitter | OnClickCallback; // Mixed using ng's Input/Output (Should care of "this" when using OnClickCallback)
+ nzOnCancel?: EventEmitter | OnClickCallback; // Mixed using ng's Input/Output (Should care of "this" when using OnClickCallback)
}
// tslint:disable-next-line:no-any