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/async.ts b/components/modal/demo/async.ts index 5e95a478eeb..13638d3a2ee 100644 --- a/components/modal/demo/async.ts +++ b/components/modal/demo/async.ts @@ -6,7 +6,7 @@ import { Component } from '@angular/core'; - +

Modal Content

`, @@ -20,7 +20,7 @@ export class NzDemoModalAsyncComponent { this.isVisible = true; } - handleOk($event: MouseEvent): void { + handleOk(): void { this.isOkLoading = true; window.setTimeout(() => { this.isVisible = false; @@ -28,7 +28,7 @@ export class NzDemoModalAsyncComponent { }, 3000); } - handleCancel($event: MouseEvent): void { + handleCancel(): void { this.isVisible = false; } } diff --git a/components/modal/demo/basic.ts b/components/modal/demo/basic.ts index bd6de93f062..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,22 +13,30 @@ 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($event: MouseEvent): void { + handleOk(): void { console.log('Button ok clicked!'); this.isVisible = false; } - handleCancel($event: MouseEvent): void { - console.log('Button cancel clicked!', $event); + handleCancel(): void { + console.log('Button cancel clicked!'); this.isVisible = false; } } diff --git a/components/modal/demo/confirm-promise.ts b/components/modal/demo/confirm-promise.ts index 6d8b0299a3b..cee05281fa2 100644 --- a/components/modal/demo/confirm-promise.ts +++ b/components/modal/demo/confirm-promise.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ModalPublicAgent, NzModalService } from 'ng-zorro-antd'; +import { NzModalRef, NzModalService } from 'ng-zorro-antd'; @Component({ selector: 'nz-demo-modal-confirm-promise', @@ -9,7 +9,7 @@ import { ModalPublicAgent, NzModalService } from 'ng-zorro-antd'; styles : [] }) export class NzDemoModalConfirmPromiseComponent { - confirmModal: ModalPublicAgent; // For testing by now + confirmModal: NzModalRef; // For testing by now constructor(private modal: NzModalService) { } diff --git a/components/modal/demo/footer.ts b/components/modal/demo/footer.ts index 83fd83b3f09..33a4f981418 100644 --- a/components/modal/demo/footer.ts +++ b/components/modal/demo/footer.ts @@ -7,7 +7,7 @@ import { Component } from '@angular/core'; - + Custom Modal Title @@ -22,8 +22,8 @@ import { Component } from '@angular/core'; Modal Footer: - - + + `, @@ -39,7 +39,7 @@ export class NzDemoModalFooterComponent { this.isVisible = true; } - handleOk($event: MouseEvent): void { + handleOk(): void { this.isConfirmLoading = true; setTimeout(() => { this.isVisible = false; @@ -47,7 +47,7 @@ export class NzDemoModalFooterComponent { }, 3000); } - handleCancel($event: MouseEvent): void { + handleCancel(): void { this.isVisible = false; } } diff --git a/components/modal/demo/service.ts b/components/modal/demo/service.ts index af58d65ec34..c26d8d9bdf4 100644 --- a/components/modal/demo/service.ts +++ b/components/modal/demo/service.ts @@ -1,7 +1,7 @@ /* entryComponents: NzModalCustomComponent */ -import { Component, Input, TemplateRef } from '@angular/core'; -import { ModalPublicAgent, NzModalService } from 'ng-zorro-antd'; +import { Component, Input, TemplateRef, ViewChild } from '@angular/core'; +import { NzModalRef, NzModalService } from 'ng-zorro-antd'; @Component({ selector: 'nz-demo-modal-service', @@ -34,11 +34,17 @@ import { ModalPublicAgent, NzModalService } from 'ng-zorro-antd'; + +

+ + + This is a non-service html modal ` }) export class NzDemoModalServiceComponent { - tplModal: ModalPublicAgent; + tplModal: NzModalRef; tplModalButtonLoading = false; + htmlModalVisible = false; constructor(private modalService: NzModalService) { } @@ -80,15 +86,20 @@ export class NzDemoModalServiceComponent { }, nzFooter: [{ label: 'change component tilte from outside', - onClick: (componentInstance: NzModalCustomComponent) => { + onClick: (componentInstance) => { componentInstance.title = 'title in inner component is changed'; } }] }); + modal.afterOpen.subscribe(() => console.log('[afterOpen] emitted!')); + + // Return a result when closed + modal.afterClose.subscribe((result) => console.log('[afterClose] The result is:', result)); + // delay until modal instance created window.setTimeout(() => { - const instance = modal.getContentComponentRef().instance as NzModalCustomComponent; + const instance = modal.getContentComponent(); instance.subtitle = 'sub title is changed'; }, 2000); } @@ -130,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({ @@ -149,9 +177,9 @@ export class NzModalCustomComponent { @Input() title: string; @Input() subtitle: string; - constructor(private modal: ModalPublicAgent) { } + constructor(private modal: NzModalRef) { } destroyModal(): void { - this.modal.destroy(); + this.modal.destroy({ data: 'this the result data' }); } } diff --git a/components/modal/doc/index.en-US.md b/components/modal/doc/index.en-US.md index c228e4f98b7..70b7f4019d9 100644 --- a/components/modal/doc/index.en-US.md +++ b/components/modal/doc/index.en-US.md @@ -14,7 +14,7 @@ getting feedback or information purposes. Additionally, if you need show a simple confirmation dialog, you can use `NzModalService.confirm()`, and so on. -It is recommended to use the `Component` way to pop up the Modal, so that the component logic of the popup layer can be completely isolated from the outer component, and can be reused at any time. In the popup layer component, you can obtain Modal's component instance by injecting `ModalPublicAgent` to control the behavior of the modal box. +It is recommended to use the `Component` way to pop up the Modal, so that the component logic of the popup layer can be completely isolated from the outer component, and can be reused at any time. In the popup layer component, you can obtain Modal's component instance by injecting `NzModalRef` to control the behavior of the modal box. ## API @@ -23,14 +23,15 @@ 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 | | nzOkLoading | Whether to apply loading visual effect for OK button or not | boolean | false | | nzCancelLoading | Whether to apply loading visual effect for Cancel button or not | boolean | false | | nzFooter | Footer content, set as footer=null when you don't need default buttons. 1. Only valid in normal mode.
2. You can customize the buttons to the maximum extent by passing a `ModalButtonOptions` configuration (see the case or the instructions below).
| string
TemplateRef
ModalButtonOptions | OK and Cancel buttons | -| nzGetContainer | The mount node for Modal | HTMLElement / () => HTMLElement| Handled by overlay container | +| nzGetContainer | The mount node for Modal | HTMLElement / () => HTMLElement| A default container | | nzMask | Whether show mask or not. | boolean | true | | nzMaskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | boolean | true | | nzMaskStyle | Style for modal's mask element. | object | - | @@ -42,8 +43,8 @@ The dialog is currently divided into 2 modes, `normal mode` and `confirm box mod | nzWidth | Width of the modal dialog. When using numbers, the default unit is `px` | string
number | 520 | | nzWrapClassName | The class name of the container of the modal dialog | string | - | | nzZIndex | The z-index of the Modal | number | 1000 | -| nzOnCancel | Specify a function that will be called when a user clicks mask, close button on top right or Cancel button. Note: When created with `NzModalService.create`, this parameter should be passed into the type of function (callback function). This function returns a promise, which is automatically closed when the execution is complete or the promise ends (return `false` to prevent closing) | EventEmitter | - | -| nzOnOk | Specify a EventEmitter that will be emitted when a user clicks the OK button | EventEmitter | 无 | +| nzOnCancel | Specify a function that will be called when a user clicks mask, close button on top right or Cancel button (If nzContent is Component, the Component instance will be put in as an argument). Note: When created with `NzModalService.create`, this parameter should be passed into the type of function (callback function). This function returns a promise, which is automatically closed when the execution is complete or the promise ends (return `false` to prevent closing) | EventEmitter | - | +| nzOnOk | Specify a EventEmitter that will be emitted when a user clicks the OK button (If nzContent is Component, the Component instance will be put in as an argument). Note: When created with `NzModalService.create`, this parameter should be passed into the type of function (callback function). This function returns a promise, which is automatically closed when the execution is complete or the promise ends (return `false` to prevent closing) | EventEmitter | 无 | | nzContent | Content | string / TemplateRef / Component / ng-content | - | | nzComponentParams | When nzContent is a Component, the attributes in this parameter will be passed to the nzContent instance | object | - | | nzIconType | Icon type of the Icon component. Only valid in confirm box mode | string | question-circle | @@ -71,8 +72,8 @@ Consistent with the above API, some property types or initial values are differe | Property | Description | Type | Default | |------------|----------------|------------------|---------------| -| nzOnOk | Specify a EventEmitter that will be emitted when a user clicks the OK button. This function returns a promise, which is automatically closed when the execution is complete or the promise ends (return `false` to prevent closing) | function | - | -| nzOnCancel | Specify a function that will be called when a user clicks mask, close button on top right or Cancel button. This function returns a promise, which is automatically closed when the execution is complete or the promise ends (return `false` to prevent closing) | function | - | +| nzOnOk | Specify a EventEmitter that will be emitted when a user clicks the OK button (If nzContent is Component, the Component instance will be put in as an argument.). This function returns a promise, which is automatically closed when the execution is complete or the promise ends (return `false` to prevent closing) | function | - | +| nzOnCancel | Specify a function that will be called when a user clicks mask, close button on top right or Cancel button (If nzContent is Component, the Component instance will be put in as an argument.). This function returns a promise, which is automatically closed when the execution is complete or the promise ends (return `false` to prevent closing) | function | - | | nzWidth | Width of the modal dialog | string / number | 416 | | nzMaskClosable | Whether to close the modal dialog when the mask (area outside the modal) is clicked | boolean | false | @@ -80,23 +81,35 @@ All the `NzModalService.method`s will return a reference, and then we can close ```ts constructor(modal: NzModalService) { - const ref: ModalPublicAgent = modal.info(); - ref.destroy(); // Note: This dialog will be destroyed directly + const ref: NzModalRef = modal.info(); + ref.close(); // Or ref.destroy(); This dialog will be destroyed directly } ``` ### Related type definition -#### ModalPublicAgent (used for control dialogs) +#### Other Methods/Attributes for NzModalService -The dialog created by the service method `NzModalService.xxx()` will return a `ModalPublicAgent` object that is used to manipulate the dialog (this object can also be obtained by dependency injection `ModalPublicAgent` if `nzContent` is used as Component) , This object has the following methods: +| Methods/Attributes | Description | Type | +|----|----| +| openModals | All currently open Modal list | NzModalRef[] | +| afterAllClose | Callback called after all Modals closed completely | Observable<void> | +| closeAll() | Close all modals | function | + +#### 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) | -| getContentComponentRef() | Gets a Component reference (of type `ComponentRef`) in the contents of the dialog for `nzContent`. Note: When the dialog is not initialized (`ngOnInit` is not executed), this function will return `undefined` | +| 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 d47fbd485ff..a8068a701a6 100644 --- a/components/modal/doc/index.zh-CN.md +++ b/components/modal/doc/index.zh-CN.md @@ -15,7 +15,7 @@ title: Modal 推荐使用加载Component的方式弹出Modal,这样弹出层的Component逻辑可以与外层Component完全隔离,并且做到可以随时复用, -在弹出层Component中可以通过依赖注入`ModalPublicAgent`方式直接获取模态框的组件实例,用于控制在弹出层组件中控制模态框行为。 +在弹出层Component中可以通过依赖注入`NzModalRef`方式直接获取模态框的组件实例,用于控制在弹出层组件中控制模态框行为。 ## API @@ -23,14 +23,15 @@ title: Modal | 参数 | 说明 | 类型 | 默认值 | |----|----|----|----| -| nzAfterClose | Modal 完全关闭后的回调 | EventEmitter | 无 | +| nzAfterOpen | Modal 打开后的回调 | EventEmitter | 无 | +| nzAfterClose | Modal 完全关闭后的回调,可监听close/destroy方法传入的参数 | EventEmitter | 无 | | nzBodyStyle | Modal body 样式 | object | 无 | | nzCancelText | 取消按钮文字。设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效) | string | 取消 | | nzClosable | 是否显示右上角的关闭按钮。确认框模式下该值无效(默认会被隐藏) | boolean | true | | nzOkLoading | 确定按钮 loading | boolean | false | | nzCancelLoading | 取消按钮 loading | boolean | false | | nzFooter | 底部内容。1. 仅在普通模式下有效。
2. 可通过传入 ModalButtonOptions 来最大程度自定义按钮(详见案例或下方说明)。
3. 当不需要底部时,可以设为 null
| string
TemplateRef
ModalButtonOptions | 默认的确定取消按钮 | -| nzGetContainer | 指定 Modal 挂载的 HTML 节点 | HTMLElement
() => HTMLElement| 默认在overlay容器中 | +| nzGetContainer | 指定 Modal 挂载的 HTML 节点 | HTMLElement
() => HTMLElement| 默认容器 | | nzMask | 是否展示遮罩 | boolean | true | | nzMaskClosable | 点击蒙层是否允许关闭 | boolean | true | | nzMaskStyle | 遮罩样式 | object | 无 | @@ -42,8 +43,8 @@ title: Modal | nzWidth | 宽度。使用数字时,默认单位为px | string
number | 520 | | nzWrapClassName | 对话框外层容器的类名 | string | 无 | | nzZIndex | 设置 Modal 的 `z-index` | number | 1000 | -| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调。注:当以`NzModalService.create`创建时,此参数应传入function(回调函数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | EventEmitter | 无 | -| nzOnOk | 点击确定回调 | EventEmitter | 无 | +| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调(若nzContent为Component,则将会以该Component实例作为参数)。注:当以`NzModalService.create`创建时,此参数应传入function(回调函数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | EventEmitter | 无 | +| nzOnOk | 点击确定回调(若nzContent为Component,则将会以该Component实例作为参数)。注:当以`NzModalService.create`创建时,此参数应传入function(回调函数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | EventEmitter | 无 | | nzContent | 内容 | string
TemplateRef
Component
ng-content | 无 | | nzComponentParams | 当nzContent为组件类(Component)时,该参数中的属性将传入nzContent实例中 | object | 无 | | nzIconType | 图标 Icon 类型。仅 确认框模式 下有效 | string | question-circle | @@ -70,8 +71,8 @@ title: Modal | 参数 | 说明 | 类型 | 默认值 | |------------|----------------|------------------|--------------| -| nzOnOk | 点击确定按钮时将执行的回调函数。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | -| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | +| nzOnOk | 点击确定按钮时将执行的回调函数(若nzContent为Component,则将会以该Component实例作为参数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | +| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调(若nzContent为Component,则将会以该Component实例作为参数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | | nzWidth | 宽度 | string
number | 416 | | nzMaskClosable | 点击蒙层是否允许关闭 | boolean | false | @@ -79,23 +80,35 @@ title: Modal ```ts constructor(modal: NzModalService) { - const ref: ModalPublicAgent = modal.info(); - ref.destroy(); // 注:这里将直接销毁对话框 + const ref: NzModalRef = modal.info(); + ref.close(); // 或 ref.destroy(); 将直接销毁对话框 } ``` ### 相关类型定义 -#### ModalPublicAgent(用于控制对话框) +#### NzModalService的其他方法/属性 -通过服务方式 `NzModalService.xxx()` 创建的对话框,都会返回一个 `ModalPublicAgent` 对象,用于操控该对话框(若使用nzContent为Component时,也可通过依赖注入 `ModalPublicAgent` 方式获得此对象),该对象具有以下方法: +| 方法/属性 | 说明 | 类型 | +|----|----| +| 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() | 关闭(隐藏)对话框。注:当用于以服务方式创建的对话框,此方法将直接 销毁 对话框(同destroy方法) | -| destroy() | 销毁对话框。注:仅用于服务方式创建的对话框(非服务方式创建的对话框,此方法只会隐藏对话框) | -| getContentComponentRef() | 获取对话框内容中`nzContent`的Component引用(类型为`ComponentRef`)。注:当对话框还未初始化完毕(`ngOnInit`未执行)时,此函数将返回`undefined` | +| close(result: any) | 关闭(隐藏)对话框。注:当用于以服务方式创建的对话框,此方法将直接 销毁 对话框(同destroy方法) | +| destroy(result: any) | 销毁对话框。注:仅用于服务方式创建的对话框(非服务方式创建的对话框,此方法只会隐藏对话框) | +| getContentComponent() | 获取对话框内容中`nzContent`的Component实例instance。注:当对话框还未初始化完毕(`ngOnInit`未执行)时,此函数将返回`undefined` | #### ModalButtonOptions(用于自定义底部按钮) diff --git a/components/modal/modal-public-agent.class.ts b/components/modal/modal-public-agent.class.ts deleted file mode 100644 index 6c6ec112fd2..00000000000 --- a/components/modal/modal-public-agent.class.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ComponentRef } from '@angular/core'; - -import { NzModalComponent } from './nz-modal.component'; - -/** - * API class that public to users to handle the modal instance. - * ModalPublicAgent is aim to avoid accessing to the modal instance directly by users. - */ -export abstract class ModalPublicAgent { - abstract open(): void; - abstract close(): void; - abstract destroy(): void; - - /** - * Return the ComponentRef of nzContent when specify nzContent as a Component - * Note: this method may return undefined if the Component has not ready yet. (it only available after Modal's ngOnInit) - */ - abstract getContentComponentRef(): ComponentRef<{}>; - - /** - * Get the dom element of this Modal - */ - abstract getElement(): HTMLElement; - - /** - * Get the instance of the Modal itself - */ - abstract getInstance(): NzModalComponent; -} 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 new file mode 100644 index 00000000000..fd5f0815e79 --- /dev/null +++ b/components/modal/nz-modal-ref.class.ts @@ -0,0 +1,39 @@ +import { ComponentRef, Type } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { NzModalComponent } from './nz-modal.component'; + +/** + * API class that public to users to handle the modal instance. + * 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; + + // /** + // * Return the ComponentRef of nzContent when specify nzContent as a Component + // * Note: this method may return undefined if the Component has not ready yet. (it only available after Modal's ngOnInit) + // */ + // abstract getContentComponentRef(): ComponentRef<{}>; + + /** + * Return the component instance of nzContent when specify nzContent as a Component + * Note: this method may return undefined if the Component has not ready yet. (it only available after Modal's ngOnInit) + */ + abstract getContentComponent(): T; + + /** + * Get the dom element of this Modal + */ + abstract getElement(): HTMLElement; + + /** + * Get the instance of the Modal itself + */ + abstract getInstance(): NzModalComponent; +} diff --git a/components/modal/nz-modal.component.html b/components/modal/nz-modal.component.html index da04a591470..2879da0a490 100644 --- a/components/modal/nz-modal.component.html +++ b/components/modal/nz-modal.component.html @@ -74,11 +74,11 @@ >{{ button.label }} - - @@ -111,11 +111,11 @@
- - diff --git a/components/modal/nz-modal.component.ts b/components/modal/nz-modal.component.ts index 20965811575..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, @@ -20,12 +21,15 @@ import { ViewChild, ViewContainerRef } 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 { ModalPublicAgent } from './modal-public-agent.class'; 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'; export const MODAL_ANIMATE_DURATION = 200; // Duration when perform animations (ms) @@ -37,19 +41,25 @@ interface ClassMap { type AnimationState = 'enter' | 'leave' | null; @Component({ - selector : 'nz-modal', + selector: 'nz-modal', templateUrl: './nz-modal.component.html' }) -export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChanges, AfterViewInit, ModalOptions { +// tslint:disable-next-line:no-any +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() 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<{}> | ModalButtonOptions[]; // [STATIC] Default Modal ONLY - @Input() nzGetContainer: HTMLElement | (() => HTMLElement); // [STATIC] + @Input() nzFooter: string | TemplateRef<{}> | Array>; // [STATIC] Default Modal ONLY + @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; @@ -57,22 +67,53 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan @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; @@ -84,35 +125,42 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan modalAnimationClassMap: object; transformOrigin = '0px 0px 0px'; // The origin point that animation based on - private contentComponentRef: ComponentRef<{}>; // Handle the reference when using nzContent as Component + 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(); } ngOnInit(): void { if (this.isComponent(this.nzContent)) { - this.createDynamicComponent(this.nzContent as Type<{}>); // Create component along without View + this.createDynamicComponent(this.nzContent as Type); // Create component along without View } if (this.isModalButtons(this.nzFooter)) { // Setup default button options - this.nzFooter = this.formatModalButtons(this.nzFooter as ModalButtonOptions[]); + 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); - } else { // Use overlay to handle this modal by default - this.overlay.create().overlayElement.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 ModalPublicAgent implements OnInit, OnChan // 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,34 +184,43 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan } } + ngOnDestroy(): void { + if (this.container instanceof OverlayRef) { + this.container.dispose(); + } + } + open(): void { this.changeVisibleFromInside(true); } - close(): void { - this.changeVisibleFromInside(false).then(() => this.nzAfterClose.emit()); + close(result?: R): void { + this.changeVisibleFromInside(false, result); } - destroy(): void { // Destroy equals Close - this.close(); + destroy(result?: R): void { // Destroy equals Close + this.close(result); } getInstance(): NzModalComponent { return this; } - getContentComponentRef(): ComponentRef<{}> { + getContentComponentRef(): ComponentRef { return this.contentComponentRef; } + getContentComponent(): T { + return this.contentComponentRef && this.contentComponentRef.instance; + } + getElement(): HTMLElement { return this.elementRef && this.elementRef.nativeElement; } onClickMask($event: MouseEvent): void { if (this.nzMask && this.nzMaskClosable && ($event.target as HTMLElement).classList.contains('ant-modal-wrap')) { - // this.close(); - this.onClickOkCancel($event, 'cancel'); + this.onClickOkCancel('cancel'); } } @@ -174,18 +228,17 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan return this.nzModalType === type; } - private onClickCloseBtn($event: MouseEvent): void { - // this.close(); - this.onClickOkCancel($event, 'cancel'); + private onClickCloseBtn(): void { + this.onClickOkCancel('cancel'); } - private onClickOkCancel($event: MouseEvent, type: 'ok' | 'cancel'): void { + private onClickOkCancel(type: 'ok' | 'cancel'): void { const trigger = { 'ok': this.nzOnOk, 'cancel': this.nzOnCancel }[ type ]; const loadingKey = { 'ok': 'nzOkLoading', 'cancel': 'nzCancelLoading' }[ type ]; if (trigger instanceof EventEmitter) { - trigger.emit($event); + trigger.emit(this.getContentComponent()); } else if (typeof trigger === 'function') { - const result = trigger($event); + const result = trigger(this.getContentComponent()); const caseClose = (doClose: boolean | void | {}) => (doClose !== false) && this.close(); // Users can return "false" to prevent closing by default if (isPromise(result)) { this[ loadingKey ] = true; @@ -216,8 +269,22 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan 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): {} { + private getButtonCallableProp(options: ModalButtonOptions, prop: string): {} { const value = options[ prop ]; const args = []; if (this.contentComponentRef) { @@ -227,7 +294,7 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan } // On nzFooter's modal button click - private onButtonClick(button: ModalButtonOptions): void { + private onButtonClick(button: ModalButtonOptions): void { const result = this.getButtonCallableProp(button, 'onClick'); // Call onClick directly if (isPromise(result)) { button.loading = true; @@ -236,13 +303,12 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan } // 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(); } @@ -275,7 +341,7 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan }, MODAL_ANIMATE_DURATION)); } - private formatModalButtons(buttons: ModalButtonOptions[]): ModalButtonOptions[] { + private formatModalButtons(buttons: Array>): Array> { return buttons.map((button) => { const mixedButton = { ...{ @@ -299,14 +365,11 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan * Create a component dynamically but not attach to any View (this action will be executed when bodyContainer is ready) * @param component Component class */ - private createDynamicComponent(component: Type<{}>): void { + private createDynamicComponent(component: Type): void { const factory = this.cfr.resolveComponentFactory(component); const childInjector = Injector.create({ - providers: [ { - provide : ModalPublicAgent, - useValue: this - } ], - parent : this.viewContainer.parentInjector + providers: [ { provide : NzModalRef, useValue: this } ], + parent: this.viewContainer.parentInjector }); this.contentComponentRef = factory.create(childInjector); if (this.nzComponentParams) { @@ -328,23 +391,16 @@ export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChan // } } - 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 b6ef8069af7..1e54338578f 100644 --- a/components/modal/nz-modal.service.ts +++ b/components/modal/nz-modal.service.ts @@ -1,10 +1,12 @@ 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 { ModalPublicAgent } from './modal-public-agent.class'; +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; - } + 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,18 +53,36 @@ 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) { } - create(options: ModalOptionsForService = {}): ModalPublicAgent { + // 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'): ModalPublicAgent { + confirm(options: ModalOptionsForService = {}, confirmType: ConfirmType = 'confirm'): NzModalRef { if ('nzFooter' in options) { this.logger.warn(`The Confirm-Modal doesn't support "nzFooter", this property will be ignored.`); } @@ -79,23 +99,23 @@ export class NzModalService { return this.create(options); } - info(options: ModalOptionsForService = {}): ModalPublicAgent { + info(options: ModalOptionsForService = {}): NzModalRef { return this.simpleConfirm(options, 'info'); } - success(options: ModalOptionsForService = {}): ModalPublicAgent { + success(options: ModalOptionsForService = {}): NzModalRef { return this.simpleConfirm(options, 'success'); } - error(options: ModalOptionsForService = {}): ModalPublicAgent { + error(options: ModalOptionsForService = {}): NzModalRef { return this.simpleConfirm(options, 'error'); } - warning(options: ModalOptionsForService = {}): ModalPublicAgent { + warning(options: ModalOptionsForService = {}): NzModalRef { return this.simpleConfirm(options, 'warning'); } - private simpleConfirm(options: ModalOptionsForService = {}, confirmType: ConfirmType): ModalPublicAgent { + private simpleConfirm(options: ModalOptionsForService = {}, confirmType: ConfirmType): NzModalRef { if (!('nzIconType' in options)) { options.nzIconType = { 'info': 'info-circle', 'success': 'check-circle', 'error': 'cross-circle', 'warning': 'exclamation-circle' }[ confirmType ]; } diff --git a/components/modal/nz-modal.spec.ts b/components/modal/nz-modal.spec.ts index 45f0bda48e2..0ede87ee160 100644 --- a/components/modal/nz-modal.spec.ts +++ b/components/modal/nz-modal.spec.ts @@ -1,20 +1,24 @@ -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 { ModalPublicAgent } from './modal-public-agent.class'; +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'; 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<{}>; @@ -57,7 +61,7 @@ describe('modal', () => { describe('demo-confirm-promise', () => { const tempModalId = generateUniqueId(); // Temp unique id to mark the confirm modal that created by service - let modalAgent: ModalPublicAgent; + let modalAgent: NzModalRef; let buttonShow: HTMLButtonElement; beforeEach(async(() => { @@ -103,7 +107,7 @@ describe('modal', () => { describe('NormalModal: created by service with most APIs', () => { const tempModalId = generateUniqueId(); // Temp unique id to mark the confirm modal that created by service - let modalAgent: ModalPublicAgent; + let modalAgent: NzModalRef; let modalElement: HTMLElement; beforeEach(async(() => { @@ -169,7 +173,7 @@ describe('modal', () => { describe('NormalModal: created by service with vary nzContent and nzFooter', () => { const tempModalId = generateUniqueId(); // Temp unique id to mark the confirm modal that created by service - let modalAgent: ModalPublicAgent; + let modalAgent: NzModalRef; let modalElement: HTMLElement; beforeEach(async(() => { @@ -193,7 +197,9 @@ describe('modal', () => { it('should change title from in/outside and trigger button', fakeAsync(() => { fixture.detectChanges(); // Initial change detecting - const contentComponent = modalAgent.getContentComponentRef().instance as TestVaryServiceCustomComponent; + 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; @@ -234,7 +240,7 @@ describe('modal', () => { spyOn(logger, 'warn'); const tempModalId = generateUniqueId(); - const modalAgent = instance.createConfirm() as ModalPublicAgent; + const modalAgent = instance.createConfirm() as NzModalRef; const modalElement = modalAgent.getElement(); modalElement.classList.add(tempModalId); fixture.detectChanges(); @@ -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 // ------------------------------------------- @@ -295,7 +438,7 @@ describe('modal', () => { - +

content

`, @@ -309,7 +452,7 @@ class NzDemoModalAsyncComponent { this.isVisible = true; } - handleOk($event: MouseEvent): void { + handleOk(): void { this.isOkLoading = true; window.setTimeout(() => { this.isVisible = false; @@ -317,7 +460,7 @@ class NzDemoModalAsyncComponent { }, 3000); } - handleCancel($event: MouseEvent): void { + handleCancel(): void { this.isVisible = false; } } @@ -330,7 +473,7 @@ class NzDemoModalAsyncComponent { styles : [] }) class NzDemoModalConfirmPromiseComponent { - confirmModal: ModalPublicAgent; // For testing by now + confirmModal: NzModalRef; // For testing by now constructor(private modal: NzModalService) { } @@ -349,7 +492,7 @@ class NzDemoModalConfirmPromiseComponent { template: `` }) class TestBasicServiceComponent { - basicModal: ModalPublicAgent; + basicModal: NzModalRef; constructor(private modalService: NzModalService) { this.modalService.create(); // [Testing Required] Only for coverage temporarily @@ -387,14 +530,14 @@ class TestBasicServiceComponent { class TestVaryServiceComponent { constructor(private modalService: NzModalService) {} - createWithVary(): ModalPublicAgent { + createWithVary(): NzModalRef { const modal = this.modalService.create({ nzContent: TestVaryServiceCustomComponent, nzComponentParams: { title: 'internal title', subtitle: 'subtitle' }, nzFooter: [ { label: 'change title from outside', - onClick: (componentInstance: TestVaryServiceCustomComponent) => { + onClick: (componentInstance) => { componentInstance.title = 'internal title changed'; return Promise.resolve(); } @@ -419,7 +562,7 @@ export class TestVaryServiceCustomComponent { @Input() title: string; @Input() subtitle: string; - constructor(private modal: ModalPublicAgent, public elementRef: ElementRef) { } + constructor(private modal: NzModalRef, public elementRef: ElementRef) { } destroyModal(): void { this.modal.destroy(); @@ -432,7 +575,7 @@ export class TestVaryServiceCustomComponent { export class TestConfirmModalComponent { constructor(public modalService: NzModalService) { } - createConfirm(): ModalPublicAgent { + createConfirm(): NzModalRef { this.modalService.confirm(); // [Testing Required] Only for coverage temporarily this.modalService.confirm({ nzWidth: 100 }); // [Testing Required] Only for coverage temporarily @@ -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 3a99bf2673c..5d4c72b270c 100644 --- a/components/modal/nz-modal.type.ts +++ b/components/modal/nz-modal.type.ts @@ -1,13 +1,14 @@ +import { OverlayRef } from '@angular/cdk/overlay'; import { EventEmitter, TemplateRef, Type } from '@angular/core'; -export type OnClickCallback = (($event: MouseEvent) => (false | void | {}) | Promise); +export type OnClickCallback = ((instance: T) => (false | void | {}) | Promise); export type ModalType = 'default' | 'confirm'; // Different modal styles we have supported export type ConfirmType = 'confirm' | 'info' | 'success' | 'error' | 'warning'; // Subtypes of Confirm Modal // Public options for using by service -export interface ModalOptions { +export interface ModalOptions { // tslint:disable-line:no-any nzModalType?: ModalType; nzVisible?: boolean; nzZIndex?: number; @@ -17,33 +18,35 @@ export interface ModalOptions { nzStyle?: object; nzIconType?: string; // Confirm Modal ONLY nzTitle?: string | TemplateRef<{}>; - nzContent?: string | TemplateRef<{}> | Type<{}>; + nzContent?: string | TemplateRef<{}> | Type; nzComponentParams?: object; nzClosable?: boolean; nzMask?: boolean; nzMaskClosable?: boolean; nzMaskStyle?: object; nzBodyStyle?: object; - nzFooter?: string | TemplateRef<{}> | ModalButtonOptions[]; // Default Modal ONLY - nzGetContainer?: HTMLElement | (() => HTMLElement); // STATIC - nzAfterClose?: EventEmitter; + nzFooter?: string | TemplateRef<{}> | Array>; // Default Modal ONLY + 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) } -export interface ModalOptionsForService extends ModalOptions { // Limitations for using by service - nzOnOk?: OnClickCallback; - nzOnCancel?: OnClickCallback; +// tslint:disable-next-line:no-any +export interface ModalOptionsForService extends ModalOptions { // Limitations for using by service + nzOnOk?: OnClickCallback; + nzOnCancel?: OnClickCallback; } -export interface ModalButtonOptions { +export interface ModalButtonOptions { // tslint:disable-line:no-any label: string; type?: string; shape?: string; @@ -52,8 +55,8 @@ export interface ModalButtonOptions { autoLoading?: boolean; // Default: true, indicate whether show loading automatically while onClick returned a Promise // [NOTE] "componentInstance" will refer to the component's instance when using Component - show?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); - loading?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); // This prop CAN'T use with autoLoading=true - disabled?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); - onClick?(this: ModalButtonOptions, contentComponentInstance?: object): (void | {}) | Promise<(void | {})>; + show?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: T) => boolean); + loading?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: T) => boolean); // This prop CAN'T use with autoLoading=true + disabled?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: T) => boolean); + onClick?(this: ModalButtonOptions, contentComponentInstance?: T): (void | {}) | Promise<(void | {})>; } diff --git a/components/modal/public-api.ts b/components/modal/public-api.ts index e009a4bf342..e6e1bf6ea2c 100644 --- a/components/modal/public-api.ts +++ b/components/modal/public-api.ts @@ -1,5 +1,5 @@ export { NzModalComponent } from './nz-modal.component'; -export { ModalPublicAgent } from './modal-public-agent.class'; +export { NzModalRef } from './nz-modal-ref.class'; export { NzModalModule } from './nz-modal.module'; export { NzModalService } from './nz-modal.service'; export * from './nz-modal.type'; diff --git a/docs/changelog.zh-CN.md b/docs/changelog.zh-CN.md index e5b894c28fe..23c4ade97f6 100755 --- a/docs/changelog.zh-CN.md +++ b/docs/changelog.zh-CN.md @@ -13,6 +13,21 @@ timeline: true --- +## 0.7.0-beta.2 + +`2018-03-19` + +#### 全局部分 + +- 去掉了 cdk-overlay-pane 的 z-index 样式 + +#### Modal + +- 新增参数`nzAfterOpen`/`nzAfterClose` +- `nzGetContainer`参数默认值改为动态创建的overlay容器 +- `NzModalService`服务新增`openModals`/`afterAllClose`/`closeAll()`支持 +- `NzModalRef`新增`afterOpen`/`afterClose`/`getContentComponent()`支持,并且原`close`/`destroy`方法支持传递参数 + ## 0.7.0-beta.1 `2018-03-15`