From 3dd535619bc73a2cb64284bd15b110d17b591acf Mon Sep 17 00:00:00 2001 From: Wilson Zeng Date: Sun, 10 Dec 2017 22:24:41 +0800 Subject: [PATCH] feat(module:nzx-modal): add(refactor) the new modal component close #317, close #644 --- src/components/ng-zorro-antd.module.ts | 6 +- src/components/nzx-modal/css-unit.pipe.ts | 12 + src/components/nzx-modal/index.ts | 1 + .../nzx-modal/modal-public-agent.class.ts | 9 + src/components/nzx-modal/modal-util.ts | 24 + .../nzx-modal/nzx-modal.component.html | 118 +++++ .../nzx-modal/nzx-modal.component.ts | 304 +++++++++++++ src/components/nzx-modal/nzx-modal.module.ts | 18 + src/components/nzx-modal/nzx-modal.service.ts | 105 +++++ .../nzx-modal/nzx-modal.service.ts.non-cdk | 95 ++++ src/components/nzx-modal/nzx-modal.type.ts | 59 +++ src/components/nzx-modal/public-api.ts | 5 + src/components/nzx-modal/style/confirm.less | 70 +++ src/components/nzx-modal/style/index.less | 3 + src/components/nzx-modal/style/modal.less | 144 ++++++ .../nz-demo-confirm-async.component.ts | 21 + .../nz-demo-confirm-basic.component.ts | 35 ++ .../nz-demo-confirm-destroy.component.ts | 22 + .../nz-demo-confirm-info.component.ts | 45 ++ .../nz-demo-modal-async.component.ts | 34 ++ .../nz-demo-modal-basic.component.ts | 33 ++ .../nz-demo-modal-customize.component.ts | 53 +++ .../nz-demo-modal-locale.component.ts | 53 +++ .../nz-demo-modal-service.component.ts | 144 ++++++ .../nz-demo-modal-style.component.ts | 63 +++ .../nzx-demo-modal/nz-demo-modal.component.ts | 19 + .../nzx-demo-modal/nz-demo-modal.html | 414 ++++++++++++++++++ .../nzx-demo-modal/nz-demo-modal.module.ts | 41 ++ .../nz-demo-modal.routing.module.ts | 12 + src/showcase/router.ts | 9 + 30 files changed, 1968 insertions(+), 3 deletions(-) create mode 100644 src/components/nzx-modal/css-unit.pipe.ts create mode 100644 src/components/nzx-modal/index.ts create mode 100644 src/components/nzx-modal/modal-public-agent.class.ts create mode 100644 src/components/nzx-modal/modal-util.ts create mode 100644 src/components/nzx-modal/nzx-modal.component.html create mode 100644 src/components/nzx-modal/nzx-modal.component.ts create mode 100644 src/components/nzx-modal/nzx-modal.module.ts create mode 100644 src/components/nzx-modal/nzx-modal.service.ts create mode 100644 src/components/nzx-modal/nzx-modal.service.ts.non-cdk create mode 100644 src/components/nzx-modal/nzx-modal.type.ts create mode 100644 src/components/nzx-modal/public-api.ts create mode 100755 src/components/nzx-modal/style/confirm.less create mode 100755 src/components/nzx-modal/style/index.less create mode 100755 src/components/nzx-modal/style/modal.less create mode 100644 src/showcase/nzx-demo-modal/nz-demo-confirm-async.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-confirm-basic.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-confirm-destroy.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-confirm-info.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal-async.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal-basic.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal-customize.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal-locale.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal-service.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal-style.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal.component.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal.html create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal.module.ts create mode 100644 src/showcase/nzx-demo-modal/nz-demo-modal.routing.module.ts diff --git a/src/components/ng-zorro-antd.module.ts b/src/components/ng-zorro-antd.module.ts index 3a442499c0c..5bf4560d3dd 100644 --- a/src/components/ng-zorro-antd.module.ts +++ b/src/components/ng-zorro-antd.module.ts @@ -33,6 +33,7 @@ import { NzMenuModule } from './menu/nz-menu.module'; import { NzMessageModule } from './message/nz-message.module'; import { NzModalModule } from './modal/nz-modal.module'; import { NzNotificationModule } from './notification/nz-notification.module'; +import { NzxModalModule } from './nzx-modal/nzx-modal.module'; import { NzPaginationModule } from './pagination/nz-pagination.module'; import { NzPopconfirmModule } from './popconfirm/nz-popconfirm.module'; import { NzPopoverModule } from './popover/nz-popover.module'; @@ -70,8 +71,7 @@ import { NzRootConfig, NZ_ROOT_CONFIG } from './root/nz-root-config'; // Mixes export * from './locale/index'; -// export { NZ_DEFAULT_LOCALE } from './locale-provider'; -// export * from './locale-provider/locale'; +export * from './nzx-modal/public-api'; export { NZ_LOGGER_STATE } from './util/logger/index'; // Modules @@ -246,7 +246,6 @@ export { NZ_ROOT_CONFIG, NzRootConfig } from './root/nz-root-config'; exports: [ LoggerModule, NzLocaleModule, - // NzLocaleProviderModule, NzButtonModule, NzAlertModule, NzBadgeModule, @@ -261,6 +260,7 @@ export { NZ_ROOT_CONFIG, NzRootConfig } from './root/nz-root-config'; NzMessageModule, NzModalModule, NzNotificationModule, + NzxModalModule, NzPaginationModule, NzPopconfirmModule, NzPopoverModule, diff --git a/src/components/nzx-modal/css-unit.pipe.ts b/src/components/nzx-modal/css-unit.pipe.ts new file mode 100644 index 00000000000..88a8b1b06f4 --- /dev/null +++ b/src/components/nzx-modal/css-unit.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'toCssUnit' +}) + +export class CssUnitPipe implements PipeTransform { + transform(value: number | string, defaultUnit: string = 'px'): string { + const formatted = +value; // force convert + return isNaN(formatted) ? `${value}` : `${formatted}${defaultUnit}`; + } +} diff --git a/src/components/nzx-modal/index.ts b/src/components/nzx-modal/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/src/components/nzx-modal/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/src/components/nzx-modal/modal-public-agent.class.ts b/src/components/nzx-modal/modal-public-agent.class.ts new file mode 100644 index 00000000000..abe29cd308c --- /dev/null +++ b/src/components/nzx-modal/modal-public-agent.class.ts @@ -0,0 +1,9 @@ +/** + * 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; +} diff --git a/src/components/nzx-modal/modal-util.ts b/src/components/nzx-modal/modal-util.ts new file mode 100644 index 00000000000..91496842957 --- /dev/null +++ b/src/components/nzx-modal/modal-util.ts @@ -0,0 +1,24 @@ +export interface ClickPosition { + x: number; + y: number; +} + +export class ModalUtil { + private lastPosition: ClickPosition = null; + + constructor(private document: Document) { + this.listenDocumentClick(); + } + + getLastClickPosition(): ClickPosition | null { + return this.lastPosition; + } + + listenDocumentClick(): void { + this.document.addEventListener('click', (event: MouseEvent) => { + this.lastPosition = { x: event.clientX, y: event.clientY }; + }); + } +} + +export default new ModalUtil(document); diff --git a/src/components/nzx-modal/nzx-modal.component.html b/src/components/nzx-modal/nzx-modal.component.html new file mode 100644 index 00000000000..319cc3dc946 --- /dev/null +++ b/src/components/nzx-modal/nzx-modal.component.html @@ -0,0 +1,118 @@ + + +
+
+ +
+ + + +
+
+ + +
+
+
+
+
+ + + +
+ +
+
+
+ +
+ + + + +
+
+
+ + + + +
+
+
+
+ + + +
+ +
+
+
+
+
+ + +
+
+
+
+ diff --git a/src/components/nzx-modal/nzx-modal.component.ts b/src/components/nzx-modal/nzx-modal.component.ts new file mode 100644 index 00000000000..da238346b05 --- /dev/null +++ b/src/components/nzx-modal/nzx-modal.component.ts @@ -0,0 +1,304 @@ +import { DOCUMENT } from '@angular/common'; +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + ComponentRef, + ElementRef, + EventEmitter, + Inject, + InjectionToken, + Injector, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + TemplateRef, + Type, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { NzLocaleService } from '../locale'; +import { ModalPublicAgent } from './modal-public-agent.class'; +import ModalUtil from './modal-util'; +import { ModalButtonOptions, ModalOptions, ModalType, OnClickCallback } from './nzx-modal.type'; + +interface ClassMap { + [index: string]: boolean; +} + +type AnimationState = 'enter' | 'leave' | null; + +@Component({ + selector: 'nzx-modal', + encapsulation: ViewEncapsulation.None, + templateUrl: './nzx-modal.component.html', + styleUrls: [ './style/index.less' ] +}) + +export class NzxModalComponent extends ModalPublicAgent implements OnInit, OnChanges, AfterViewInit, 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<{}> | ModalButtonOptions[]; // [STATIC] Default Modal ONLY + @Input() nzGetContainer: HTMLElement | (() => HTMLElement) = this.document.body; // [STATIC] + + @Input() nzVisible = false; + @Output() nzVisibleChange = new EventEmitter(); + @Input() nzZIndex: number = 1000; + @Input() nzWidth: number | string = 520; + @Input() nzWrapClassName: string; + @Input() nzClassName: string; + @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() nzMaskStyle: object; + @Input() nzBodyStyle: object; + @Output() nzAfterClose = new EventEmitter(); // Trigger when modal is hidden + + // --- Predefined OK & Cancel buttons + @Input() nzOkText: string = this.locale.translate('Modal.okText'); + @Input() nzOkType = 'primary'; + @Input() nzOkLoading = false; + @Input() @Output() nzOnOk = new EventEmitter(); + @ViewChild('autoFocusButtonOk', { read: ElementRef }) autoFocusButtonOk: ElementRef; // Only aim to focus the ok button that needs to be auto focused + @Input() nzCancelText: string = this.locale.translate('Modal.cancelText'); + @Input() nzCancelLoading = false; + @Input() @Output() nzOnCancel = new EventEmitter(); + + @ViewChild('modalContainer') modalContainer: ElementRef; + @ViewChild('bodyContainer', { read: ViewContainerRef }) bodyContainer: ViewContainerRef; + + private maskAnimationClassMap: object; + private modalAnimationClassMap: object; + private get hidden(): boolean { return !this.nzVisible && !this.animationState; } // Indicate whether this dialog should hidden + + private contentComponentRef: ComponentRef<{}>; // Handle the reference when using nzContent as Component + private animationState: AnimationState; // Current animation state + private transformOrigin = '0px 0px 0px'; // The origin point that animation based on + + constructor( + private locale: NzLocaleService, + private cfr: ComponentFactoryResolver, + private elementRef: ElementRef, + @Inject(DOCUMENT) private document: any // tslint:disable-line:no-any + ) { + super(); + } + + ngOnInit(): void { + if (this.isComponent(this.nzContent)) { + // [NEED OPTIMATION] Unpleasure way to create component in next detection loop + // [NOTE] Why? When we use structure template, we couldn't retrieve bodyContainer at ngOnInit, but if we create it under ngAfterViewInit, it will cause multi-change detection error (because we create new component in a detection loop) + window.setTimeout(() => this.createDynamicComponent(this.nzContent as Type<{}>)); + } + + if (this.isModalButtons(this.nzFooter)) { // Setup default button options + this.nzFooter = this.formatModalButtons(this.nzFooter as ModalButtonOptions[]); + } + + const container = typeof this.nzGetContainer === 'function' ? this.nzGetContainer() : this.nzGetContainer; + if (container instanceof HTMLElement) { + container.appendChild(this.elementRef.nativeElement); + } + } + + // [NOTE] NOT available when using by service! + // Because ngOnChanges never be called when using by service, + // here we can't support "nzContent"(Component) etc. as inputs that initialized dynamically. + // 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); + } + } + } + + ngAfterViewInit(): void { + if (this.autoFocusButtonOk) { + (this.autoFocusButtonOk.nativeElement as HTMLButtonElement).focus(); + } + } + + open(): void { + this.changeVisibleFromInside(true); + } + + close(): void { + this.changeVisibleFromInside(false).then(() => this.nzAfterClose.emit()); + } + + destroy(): void { // Destroy equals Close + this.close(); + } + + private onClickMask($event: MouseEvent): void { + if (this.nzMask && this.nzMaskClosable && ($event.target as HTMLElement).classList.contains('ant-modal-wrap')) { + // this.close(); + this.onClickOkCancel($event, 'cancel'); + } + } + + private onClickCloseBtn($event: MouseEvent): void { + // this.close(); + this.onClickOkCancel($event, 'cancel'); + } + + private onClickOkCancel($event: MouseEvent, 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); + } else if (typeof trigger === 'function') { + const result = trigger($event); + const caseClose = (doClose: boolean | void | {}) => (doClose !== false) && this.close(); // Users can return "false" to prevent closing by default + if (isPromise(result)) { + this[ loadingKey ] = true; + const handleThen = (doClose) => { this[ loadingKey ] = false; caseClose(doClose); }; + (result as Promise).then(handleThen).catch(handleThen); + } else { + caseClose(result); + } + } + } + + private isModalType(type: ModalType): boolean { + return this.nzModalType === type; + } + + private isNonEmptyString(value: {}): boolean { + return typeof value === 'string' && value !== ''; + } + + private isTemplateRef(value: {}): boolean { + return value instanceof TemplateRef; + } + + private isComponent(value: {}): boolean { + return value instanceof Type; + } + + private isModalButtons(value: {}): boolean { + return Array.isArray(value) && value.length > 0; + } + + // 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, defaultValue?: {}): {} { + let value; + if (typeof options[prop] === 'undefined') { + if (arguments.length === 3) { value = defaultValue; } + } else { + value = options[prop]; + } + const args = []; + if (this.contentComponentRef) { args.push(this.contentComponentRef.instance); } + return typeof value === 'function' ? value.apply(options, args) : options[prop]; + } + + // On nzFooter's modal button click + private onButtonClick(button: ModalButtonOptions): void { + const result = this.getButtonCallableProp(button, 'onClick'); // Call onClick directly + if (isPromise(result)) { + button.loading = true; + (result as Promise<{}>).then(() => button.loading = false).catch(() => button.loading = false); + } + } + + // Change nzVisible from inside + private changeVisibleFromInside(visible: boolean): 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 Promise.resolve(); + } + + private changeAnimationState(state: AnimationState): void { + this.animationState = state; + if (state) { + this.maskAnimationClassMap = { + [`fade-${state}`]: true, + [`fade-${state}-active`]: true, + }; + this.modalAnimationClassMap = { + [`zoom-${state}`]: true, + [`zoom-${state}-active`]: true, + }; + } else { + this.maskAnimationClassMap = this.modalAnimationClassMap = null; + } + } + + private animateTo(isVisible: boolean): Promise { + if (isVisible) { // Figure out the lastest click position when shows up + window.setTimeout(() => this.updateTransformOrigin()); // [NOTE] Using timeout due to the document.click event is fired later than visible change, so if not postponed to next event-loop, we can't get the lastest click position + } + + this.changeAnimationState(isVisible ? 'enter' : 'leave'); + return new Promise((resolve) => window.setTimeout(() => { // Return when animation is over + this.changeAnimationState(null); + resolve(); + }, 200)); + } + + private formatModalButtons(buttons: ModalButtonOptions[]): ModalButtonOptions[] { + return buttons.map((button) => { + const mixedButton = { + ...{ + type: 'default', + size: 'large', + autoLoading: true, + show: true, + loading: false, + disabled: false, + }, + ...button + }; + + // if (mixedButton.autoLoading) { mixedButton.loading = false; } // Force loading to false when autoLoading=true + + return mixedButton; + }); + } + + private createDynamicComponent(component: Type<{}>): void { + const factory = this.cfr.resolveComponentFactory(component); + const childInjector = Injector.create([{ provide: ModalPublicAgent, useValue: this }], this.bodyContainer.parentInjector); + this.contentComponentRef = this.bodyContainer.createComponent(factory, null, childInjector); + if (this.nzComponentParams) { + Object.assign(this.contentComponentRef.instance, this.nzComponentParams); + } + } + + // Update transform-origin to the last click position on document + private updateTransformOrigin(): void { + const modalElement = this.modalContainer.nativeElement as HTMLElement; + const lastPosition = ModalUtil.getLastClickPosition(); + if (lastPosition) { + this.transformOrigin = `${lastPosition.x - modalElement.offsetLeft}px ${lastPosition.y - modalElement.offsetTop}px 0px`; + } else { + this.transformOrigin = '0px 0px 0px'; + } + } + + // TODO: We should detect if there are modals remained in this page, if 0 modals that we chould to remove overflow, otherwise, we should leave it 'hidden'. + private changeBodyOverflow(visible: boolean): void { + this.document.body.style.overflow = visible ? 'hidden' : ''; + } +} + +//////////// + +function isPromise(obj: {} | void): boolean { + return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof (obj as Promise<{}>).then === 'function' && typeof (obj as Promise<{}>).catch === 'function'; +} diff --git a/src/components/nzx-modal/nzx-modal.module.ts b/src/components/nzx-modal/nzx-modal.module.ts new file mode 100644 index 00000000000..79cab6e53e9 --- /dev/null +++ b/src/components/nzx-modal/nzx-modal.module.ts @@ -0,0 +1,18 @@ +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NzButtonModule } from '../button/nz-button.module'; +import { NzLocaleModule } from '../locale'; +import { LoggerModule } from '../util/logger/logger.module'; +import { CssUnitPipe } from './css-unit.pipe'; +import { NzxModalComponent } from './nzx-modal.component'; +import { NzxModalService } from './nzx-modal.service'; + +@NgModule({ + imports: [ CommonModule, OverlayModule, NzLocaleModule, NzButtonModule, LoggerModule ], + exports: [ NzxModalComponent ], + declarations: [ NzxModalComponent, CssUnitPipe ], + entryComponents: [ NzxModalComponent ], + providers: [ NzxModalService ], +}) +export class NzxModalModule { } diff --git a/src/components/nzx-modal/nzx-modal.service.ts b/src/components/nzx-modal/nzx-modal.service.ts new file mode 100644 index 00000000000..03354c34fc0 --- /dev/null +++ b/src/components/nzx-modal/nzx-modal.service.ts @@ -0,0 +1,105 @@ +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 { LoggerService } from '../util/logger/logger.service'; +import { ModalPublicAgent } from './modal-public-agent.class'; +import { NzxModalComponent } from './nzx-modal.component'; +import { ConfirmType, ModalOptions, ModalOptionsForService } from './nzx-modal.type'; + +// A builder used for managing service creating modals +export class ModalBuilderForService { + private modalRef: ComponentRef; // Modal ComponentRef, "null" means it has been destroyed + private overlayRef: OverlayRef; + + constructor(private overlay: Overlay, options: ModalOptionsForService = {}) { + this.createModal(); + + if (!('nzGetContainer' in options)) { // As we use CDK to create modal in service, there is no need to append DOM to body by default + options.nzGetContainer = null; + } + + 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(): NzxModalComponent { + return this.modalRef && this.modalRef.instance; + } + + destroyModal(): void { + if (this.modalRef) { + this.overlayRef.dispose(); + this.modalRef = null; + } + } + + private changeProps(options: ModalOptions): void { + if (this.modalRef) { + Object.assign(this.modalRef.instance, options); // DANGER: here not limit user's inputs at runtime + } + } + + // Create component to ApplicationRef + private createModal(): void { + this.overlayRef = this.overlay.create(); + this.modalRef = this.overlayRef.attach(new ComponentPortal(NzxModalComponent)); + } +} + +@Injectable() +export class NzxModalService { + + constructor(private overlay: Overlay, private logger: LoggerService) { } + + create(options: ModalOptionsForService = {}): ModalPublicAgent { + if (typeof options.nzOnCancel !== 'function') { + options.nzOnCancel = () => {}; // Leave a empty function to close this modal by default + } + + return new ModalBuilderForService(this.overlay, options).getInstance(); + } + + confirm(options: ModalOptionsForService = {}, confirmType: ConfirmType = 'confirm'): ModalPublicAgent { + if ('nzFooter' in options) { + this.logger.warn(`The Confirm-Modal doesn't support "nzFooter", this property will be ignored.`); + } + if (!('nzWidth' in options)) { + options.nzWidth = 416; + } + if (typeof options.nzOnOk !== 'function') { + options.nzOnOk = () => {}; // Leave a empty function to close this modal by default + } + + options.nzModalType = 'confirm'; + options.nzClassName = `ant-confirm ant-confirm-${confirmType} ${options.nzClassName || ''}`; + options.nzMaskClosable = false; + return this.create(options); + } + + info(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'info'); + } + + success(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'success'); + } + + error(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'error'); + } + + warning(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'warning'); + } + + private simpleConfirm(options: ModalOptionsForService = {}, confirmType: ConfirmType): ModalPublicAgent { + if (!('nzIconType' in options)) { + options.nzIconType = { 'info': 'info-circle', 'success': 'check-circle', 'error': 'cross-circle', 'warning': 'exclamation-circle' }[ confirmType ]; + } + if (!('nzCancelText' in options)) { // Remove the Cancel button if the user not specify a Cancel button + options.nzCancelText = null; + } + return this.confirm(options, confirmType); + } +} diff --git a/src/components/nzx-modal/nzx-modal.service.ts.non-cdk b/src/components/nzx-modal/nzx-modal.service.ts.non-cdk new file mode 100644 index 00000000000..8f7d8822c94 --- /dev/null +++ b/src/components/nzx-modal/nzx-modal.service.ts.non-cdk @@ -0,0 +1,95 @@ +import { ApplicationRef, ComponentFactoryResolver, ComponentRef, EventEmitter, Injectable, Injector, TemplateRef, Type } from '@angular/core'; +import { ModalPublicAgent } from './modal-public-agent.class'; +import { NzxModalComponent } from './nzx-modal.component'; +import { ModalOptions, ModalOptionsForService } from './nzx-modal.type'; + +// A builder used for managing service creating modals +export class ModalBuilderForService { + private modalRef: ComponentRef; // Modal ComponentRef, "null" means it has been destroyed + // private modalAgent: NzxModalAgent; + + constructor( + private cfr: ComponentFactoryResolver, + private appRef: ApplicationRef, + private injector: Injector, + options?: ModalOptionsForService) { + + this.modalRef = this.createModal(this.cfr, this.appRef, this.injector); + if (options) { + this.changeProps(options); + } + // this.modalRef.instance.registerDestroyFn(() => this.destroy()); + // this.modalRef.instance.registerModalAgent(this.modalAgent = new NzxModalAgent(this)); + this.modalRef.instance.open(); + this.modalRef.instance.nzAfterClose.subscribe(() => this.destroyModal()); // [NOTE] By default, hidden means destroy when using as Service + } + + // // Get properties from modal component instance + // // [NOTE] It could be better to limit the keys the users can access + // get(key: string): void { + // if (this.modalRef) { + // return this.modalRef.instance[key]; + // } + // } + + // getAgent(): NzxModalAgent { + // return this.modalAgent; + // } + + getInstance(): NzxModalComponent { + return this.modalRef && this.modalRef.instance; + } + + destroyModal(): void { + if (this.modalRef) { + this.appRef.detachView(this.modalRef.hostView); + this.modalRef.destroy(); + + this.modalRef = null; + } + } + + private changeProps(options: ModalOptions): void { + if (this.modalRef) { + Object.assign(this.modalRef.instance, options); // DANGER: here not limit user's inputs at runtime + } + } + + // Create component to ApplicationRef + private createModal(cfr: ComponentFactoryResolver, appRef: ApplicationRef, injector: Injector): ComponentRef { + const factory = this.cfr.resolveComponentFactory(NzxModalComponent); + const componentRef = factory.create(this.injector); + this.appRef.attachView(componentRef.hostView); + return componentRef; + } +} + +@Injectable() +export class NzxModalService { + + constructor( + private cfr: ComponentFactoryResolver, + private appRef: ApplicationRef, + private injector: Injector) { } + + create(options?: ModalOptionsForService): ModalPublicAgent { + return new ModalBuilderForService(this.cfr, this.appRef, this.injector, options).getInstance(); + } + + confirm(options: ModalOptionsForService = {}): ModalPublicAgent { + if ('nzFooter' in options) { + console.warn(`The Confirm-Modal doesn't support "nzFooter", this property will be ignored.`); + } + if (typeof options.nzOnOk !== 'function') { + options.nzOnOk = () => {}; // Leave a empty function to close this modal by default + } + if (typeof options.nzOnCancel !== 'function') { + options.nzOnCancel = () => {}; // Leave a empty function to close this modal by default + } + + options.nzModalType = 'confirm'; + options.nzClassName = `ant-confirm ant-confirm-confirm ${options.nzClassName || ''}`; + options.nzMaskClosable = false; + return this.create(options); + } +} diff --git a/src/components/nzx-modal/nzx-modal.type.ts b/src/components/nzx-modal/nzx-modal.type.ts new file mode 100644 index 00000000000..e3ee0b75278 --- /dev/null +++ b/src/components/nzx-modal/nzx-modal.type.ts @@ -0,0 +1,59 @@ +import { EventEmitter, TemplateRef, Type } from '@angular/core'; + +export type OnClickCallback = (($event: MouseEvent) => void | PromiseLike); + +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 { + nzModalType?: ModalType; + nzVisible?: boolean; + nzZIndex?: number; + nzWidth?: number | string; + nzWrapClassName?: string; + nzClassName?: string; + nzStyle?: object; + nzIconType?: string; // Confirm Modal ONLY + nzTitle?: string | TemplateRef<{}>; + 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; + + // --- 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) + nzCancelText?: string; + nzCancelLoading?: boolean; + 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; +} + +export interface ModalButtonOptions { + label: string; + type?: string; + shape?: string; + ghost?: boolean; + size?: string; + 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 | {}; +} diff --git a/src/components/nzx-modal/public-api.ts b/src/components/nzx-modal/public-api.ts new file mode 100644 index 00000000000..e1ae1b05c40 --- /dev/null +++ b/src/components/nzx-modal/public-api.ts @@ -0,0 +1,5 @@ +export { NzxModalComponent } from './nzx-modal.component'; +export { ModalPublicAgent } from './modal-public-agent.class'; +export { NzxModalModule } from './nzx-modal.module'; +export { NzxModalService } from './nzx-modal.service'; +export * from './nzx-modal.type'; diff --git a/src/components/nzx-modal/style/confirm.less b/src/components/nzx-modal/style/confirm.less new file mode 100755 index 00000000000..a2170172398 --- /dev/null +++ b/src/components/nzx-modal/style/confirm.less @@ -0,0 +1,70 @@ +@import "../../style/mixins/index"; + +@confirm-prefix-cls: ~"@{ant-prefix}-confirm"; + +.@{confirm-prefix-cls} { + .@{ant-prefix}-modal-header { + display: none; + } + + .@{ant-prefix}-modal-close { + display: none; + } + + .@{ant-prefix}-modal-body { + padding: 30px 40px; + } + + &-body-wrapper { + .clearfix(); + } + + &-body { + .@{confirm-prefix-cls}-title { + color: @text-color; + font-weight: bold; + font-size: @font-size-lg; + } + + .@{confirm-prefix-cls}-content { + margin-left: 42px; + font-size: @font-size-base; + color: @text-color; + margin-top: 8px; + } + + > .@{iconfont-css-prefix} { + font-size: 24px; + margin-right: 16px; + padding: 0 1px; + float: left; + } + } + + .@{confirm-prefix-cls}-btns { + margin-top: 30px; + float: right; + + button + button { + margin-left: 10px; + margin-bottom: 0; + } + } + + &-error &-body > .@{iconfont-css-prefix} { + color: @error-color; + } + + &-warning &-body > .@{iconfont-css-prefix}, + &-confirm &-body > .@{iconfont-css-prefix} { + color: @warning-color; + } + + &-info &-body > .@{iconfont-css-prefix} { + color: @info-color; + } + + &-success &-body > .@{iconfont-css-prefix} { + color: @success-color; + } +} diff --git a/src/components/nzx-modal/style/index.less b/src/components/nzx-modal/style/index.less new file mode 100755 index 00000000000..17d29353d42 --- /dev/null +++ b/src/components/nzx-modal/style/index.less @@ -0,0 +1,3 @@ +@import "../../style/themes/default"; +@import "./modal"; +@import "./confirm"; diff --git a/src/components/nzx-modal/style/modal.less b/src/components/nzx-modal/style/modal.less new file mode 100755 index 00000000000..755717e913c --- /dev/null +++ b/src/components/nzx-modal/style/modal.less @@ -0,0 +1,144 @@ +@dialog-prefix-cls: ~"@{ant-prefix}-modal"; + +.@{dialog-prefix-cls} { + position: relative; + width: auto; + margin: 0 auto; + top: 100px; + padding-bottom: 24px; + + &-wrap { + position: fixed; + overflow: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal; + -webkit-overflow-scrolling: touch; + outline: 0; + } + + &-title { + margin: 0; + font-size: @font-size-lg; + line-height: 21px; + font-weight: 500; + color: @heading-color; + } + + &-content { + position: relative; + background-color: @component-background; + border: 0; + border-radius: @border-radius-base; + background-clip: padding-box; + box-shadow: @shadow-2; + } + + &-close { + cursor: pointer; + border: 0; + background: transparent; + position: absolute; + right: 0; + top: 0; + z-index: 10; + font-weight: 700; + line-height: 1; + text-decoration: none; + transition: color .3s ease; + color: @text-color-secondary; + outline: 0; + + &-x { + display: block; + font-style: normal; + vertical-align: baseline; + text-align: center; + text-transform: none; + text-rendering: auto; + width: 48px; + height: 48px; + line-height: 48px; + font-size: @font-size-lg; + + &:before { + content: "\e633"; + display: block; + font-family: "anticon" !important; + } + } + + &:focus, + &:hover { + color: #444; + text-decoration: none; + } + } + + &-header { + padding: 13px 16px; + border-radius: @border-radius-base @border-radius-base 0 0; + background: @component-background; + color: @text-color; + border-bottom: @border-width-base @border-style-base @border-color-split; + } + + &-body { + padding: 16px; + font-size: @font-size-base; + line-height: 1.5; + } + + &-footer { + border-top: @border-width-base @border-style-base @border-color-split; + padding: 10px 16px 10px 10px; + text-align: right; + border-radius: 0 0 @border-radius-base @border-radius-base; + button + button { + margin-left: 8px; + margin-bottom: 0; + } + } + + &.zoom-enter, + &.zoom-appear { + animation-duration: @animation-duration-slow; + transform: none; // reset scale avoid mousePosition bug + opacity: 0; + } + + &-mask { + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 0; + background-color: #373737; + background-color: @modal-mask-bg; // lesshint duplicateProperty: false + height: 100%; + z-index: @zindex-modal-mask; + filter: ~"alpha(opacity=50)"; + + &-hidden { + display: none; + } + } + + &-open { + overflow: hidden; + } +} + +@media (max-width: 768px) { + .@{dialog-prefix-cls} { + width: auto !important; + margin: 10px; + } + .vertical-center-modal { + .@{dialog-prefix-cls} { + flex: 1; + } + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-confirm-async.component.ts b/src/showcase/nzx-demo-modal/nz-demo-confirm-async.component.ts new file mode 100644 index 00000000000..e0a048ec479 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-confirm-async.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { NzxModalService } from '../../../index.showcase'; + +@Component({ + selector: 'nz-demo-confirm-async', + template: ` + + `, + styles : [] +}) +export class NzDemoConfirmAsyncComponent { + constructor(private modal: NzxModalService) { } + + showConfirm(): void { + this.modal.confirm({ + nzTitle: 'Do you Want to delete these items?', + nzContent: 'When clicked the OK button, this dialog will be closed after 1 second', + nzOnOk: () => new Promise(resolve => window.setTimeout(resolve, 1000)) + }); + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-confirm-basic.component.ts b/src/showcase/nzx-demo-modal/nz-demo-confirm-basic.component.ts new file mode 100644 index 00000000000..b147f0bea21 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-confirm-basic.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { NzxModalService } from '../../../index.showcase'; + + +@Component({ + selector: 'nz-demo-confirm-basic', + template: ` + + + `, + styles : [] +}) +export class NzDemoConfirmBasicComponent { + constructor(private modalService: NzxModalService) { } + + showConfirm(): void { + this.modalService.confirm({ + nzTitle: 'Do you Want to delete these items?', + nzContent: 'Some descriptions', + nzOnOk: () => console.log('OK') + }); + } + + showDeleteConfirm(): void { + this.modalService.confirm({ + nzTitle: 'Are you sure delete this task?', + nzContent: 'Some descriptions', + nzOkText: 'Yes', + nzOkType: 'danger', + nzOnOk: () => console.log('OK'), + nzCancelText: 'No', + nzOnCancel: () => console.log('Cancel') + }); + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-confirm-destroy.component.ts b/src/showcase/nzx-demo-modal/nz-demo-confirm-destroy.component.ts new file mode 100644 index 00000000000..e4789e45eba --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-confirm-destroy.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { NzxModalService } from '../../../index.showcase'; + +@Component({ + selector: 'nz-demo-confirm-destroy', + template: ` + + `, + styles: [] +}) +export class NzDemoConfirmDestroyComponent { + constructor(private modalService: NzxModalService) { } + + success(): void { + const modal = this.modalService.success({ + nzTitle: 'This is a notification message', + nzContent: 'This modal will be destroyed after 1 second' + }); + + window.setTimeout(() => modal.close(), 1000); + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-confirm-info.component.ts b/src/showcase/nzx-demo-modal/nz-demo-confirm-info.component.ts new file mode 100644 index 00000000000..cfef7301be0 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-confirm-info.component.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { NzxModalService } from '../../../index.showcase'; + +@Component({ + selector: 'nz-demo-confirm-info', + template: ` + + + + + `, + styles: [] +}) +export class NzDemoConfirmInfoComponent { + constructor(private modalService: NzxModalService) { } + + info(): void { + this.modalService.info({ + nzTitle: 'This is a notification message', + nzContent: '

some messages...some messages...

some messages...some messages...

', + nzOnOk: () => console.log('Info OK') + }); + } + + success(): void { + this.modalService.success({ + nzTitle: 'This is a success message', + nzContent: 'some messages...some messages...' + }); + } + + error(): void { + this.modalService.error({ + nzTitle: 'This is an error message', + nzContent: 'some messages...some messages...' + }); + } + + warning(): void { + this.modalService.warning({ + nzTitle: 'This is an warning message', + nzContent: 'some messages...some messages...' + }); + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal-async.component.ts b/src/showcase/nzx-demo-modal/nz-demo-modal-async.component.ts new file mode 100644 index 00000000000..fdfa52dcd88 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal-async.component.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-modal-async', + template: ` + + +

对话框的内容

+
+ `, + styles: [] +}) +export class NzDemoModalAsyncComponent { + isVisible = false; + isOkLoading = false; + + showModal(): void { + this.isVisible = true; + } + + handleOk($event: MouseEvent): void { + this.isOkLoading = true; + window.setTimeout(() => { + this.isVisible = false; + this.isOkLoading = false; + }, 3000); + } + + handleCancel($event: MouseEvent): void { + this.isVisible = false; + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal-basic.component.ts b/src/showcase/nzx-demo-modal/nz-demo-modal-basic.component.ts new file mode 100644 index 00000000000..00d71b0898b --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal-basic.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-modal-basic', + template: ` + + +

Content one

+

Content two

+

Content three

+
+ `, + styles: [] +}) +export class NzDemoModalBasicComponent { + isVisible = false; + + constructor() {} + + showModal(): void { + this.isVisible = true; + } + + handleOk($event: MouseEvent): void { + console.log('Button ok clicked!'); + this.isVisible = false; + } + + handleCancel($event: MouseEvent): void { + console.log('Button cancel clicked!', $event); + this.isVisible = false; + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal-customize.component.ts b/src/showcase/nzx-demo-modal/nz-demo-modal-customize.component.ts new file mode 100644 index 00000000000..f5878bd3eb0 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal-customize.component.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; + + +@Component({ + selector: 'nz-demo-modal-customize', + template: ` + + + + 自定义对话框标题 + + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ + + 自定义底部: + + + +
+ `, + styles: [] +}) +export class NzDemoModalCustomizeComponent { + isVisible = false; + isConfirmLoading = false; + + constructor() { } + + showModal(): void { + this.isVisible = true; + } + + handleOk($event: MouseEvent): void { + this.isConfirmLoading = true; + setTimeout(() => { + this.isVisible = false; + this.isConfirmLoading = false; + }, 3000); + } + + handleCancel($event: MouseEvent): void { + this.isVisible = false; + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal-locale.component.ts b/src/showcase/nzx-demo-modal/nz-demo-modal-locale.component.ts new file mode 100644 index 00000000000..e4fa347ca2b --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal-locale.component.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; +import { NzxModalService } from '../../../index.showcase'; + + +@Component({ + selector: 'nz-demo-modal-locale', + template: ` +
+ + +

Bla bla ...

+

Bla bla ...

+

Bla bla ...

+
+
+
+ + `, + styles: [] +}) +export class NzDemoModalLocaleComponent { + isVisible = false; + + constructor(private modalService: NzxModalService) { } + + showModal(): void { + this.isVisible = true; + } + + handleOk(): void { + this.isVisible = false; + } + + handleCancel(): void { + this.isVisible = false; + } + + showConfirm(): void { + this.modalService.confirm({ + nzTitle: 'Confirm', + nzContent: 'Bla bla ...', + nzOkText: '确认', + nzCancelText: '取消' + }); + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal-service.component.ts b/src/showcase/nzx-demo-modal/nz-demo-modal-service.component.ts new file mode 100644 index 00000000000..e4bda0ad861 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal-service.component.ts @@ -0,0 +1,144 @@ +import { Component, Input, TemplateRef } from '@angular/core'; +import { ModalPublicAgent, NzxModalService } from '../../../index.showcase'; +import { THIS_EXPR } from '../../../integration/angular-cli/node_modules/@angular/compiler/src/output/output_ast'; + +@Component({ + selector: 'nz-demo-modal-service', + template: ` + + + + + 对话框标题模板 + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ + + + + + + + ` +}) +export class NzDemoModalServiceComponent { + tplModal: ModalPublicAgent; + tplModalButtonLoading = false; + + constructor(private modalService: NzxModalService) { } + + createModal(): void { + this.modalService.create({ + nzTitle: '对话框标题', + nzContent: '纯文本内容,点确认 1 秒后关闭', + nzClosable: false, + nzOnOk: () => new Promise((resolve) => window.setTimeout(resolve, 1000)) + }); + } + + createTplModal(tplTitle: TemplateRef<{}>, tplContent: TemplateRef<{}>, tplFooter: TemplateRef<{}>): void { + this.tplModal = this.modalService.create({ + nzTitle: tplTitle, + nzContent: tplContent, + nzFooter: tplFooter, + nzMaskClosable: false, + nzOnOk: () => console.log('Click ok') + }); + } + + destroyTplModal(): void { + this.tplModalButtonLoading = true; + window.setTimeout(() => { + this.tplModalButtonLoading = false; + this.tplModal.destroy(); + }, 1000); + } + + createComponentModal(): void { + this.modalService.create({ + nzTitle: '对话框标题', + nzContent: NzModalCustomComponent, + nzComponentParams: { + title: '这是Component内部标题' + }, + nzFooter: [{ + label: '从外部改变Component标题', + onClick: (componentInstance: NzModalCustomComponent) => { + componentInstance.title = '内部Component标题被改变啦!!!!!!!!!'; + } + }], + }); + } + + createCustomButtonModal(): void { + const modal = this.modalService.create({ + nzTitle: '自定义按钮举例', + nzContent: '通过传入按钮配置数组到nzFooter,用于创建多个自定义按钮', + nzFooter: [ + { + label: 'X', + shape: 'circle', + onClick: () => modal.destroy(), + }, + { + label: '弹出确认框', + type: 'primary', + onClick: () => this.modalService.confirm({ nzTitle: '确认框标题!', nzContent: '确认框描述' }), + }, + { + label: '自动改变按钮状态', + type: 'danger', + loading: false, + onClick(): void { // 注:这里由于要得到this,所以不能用箭头函数 + this.loading = true; + window.setTimeout(() => this.loading = false, 1000); + window.setTimeout(() => { + this.loading = false; + this.disabled = true; + this.label = '不能点击了!'; + }, 2000); + } + }, + { + label: '异步加载', + type: 'dashed', + onClick: () => new Promise(resolve => window.setTimeout(resolve, 2000)) + } + ] + }); + } +} + +@Component({ + selector: 'nz-modal-custom-component', + template: ` +
+

{{ title }}

+

+ 可以在弹出框中的Component内访问到模态框实例 + +

+
+ ` +}) +export class NzModalCustomComponent { + @Input() title: string; + + constructor(private modal: ModalPublicAgent) { } + + destroyModal(): void { + this.modal.destroy(); + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal-style.component.ts b/src/showcase/nzx-demo-modal/nz-demo-modal-style.component.ts new file mode 100644 index 00000000000..1135af9369e --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal-style.component.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-modal-style', + template: ` + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ +

+ + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ `, + styles: [ ` + ::ng-deep .vertical-center-modal { + display: flex; + align-items: center; + justify-content: center; + } + + ::ng-deep .vertical-center-modal .ant-modal { + top: 0; + } + ` ] +}) +export class NzDemoModalStyleComponent { + isVisibleTop = false; + isVisibleMiddle = false; + + showModalTop(): void { + this.isVisibleTop = true; + } + + showModalMiddle(): void { + this.isVisibleMiddle = true; + } + + handleOkTop(): void { + console.log('点击了确定'); + this.isVisibleTop = false; + } + + handleCancelTop(): void { + this.isVisibleTop = false; + } + + handleOkMiddle(): void { + console.log('点击了确定'); + this.isVisibleMiddle = false; + } + + handleCancelMiddle(): void { + this.isVisibleMiddle = false; + } +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal.component.ts b/src/showcase/nzx-demo-modal/nz-demo-modal.component.ts new file mode 100644 index 00000000000..2a39bfa7ea4 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal.component.ts @@ -0,0 +1,19 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + +@Component({ + selector : 'nz-demo-modal', + encapsulation: ViewEncapsulation.None, + templateUrl : './nz-demo-modal.html', +}) +export class NzDemoModalComponent { + NzDemoModalBasicCode = require('!!raw-loader!./nz-demo-modal-basic.component'); + NzDemoModalCustomizeCode = require('!!raw-loader!./nz-demo-modal-customize.component'); + NzDemoModalAsyncCode = require('!!raw-loader!./nz-demo-modal-async.component'); + NzDemoConfirmBasicCode = require('!!raw-loader!./nz-demo-confirm-basic.component'); + NzDemoConfirmAsyncCode = require('!!raw-loader!./nz-demo-confirm-async.component'); + NzDemoConfirmInfoCode = require('!!raw-loader!./nz-demo-confirm-info.component'); + NzDemoModalLocaleCode = require('!!raw-loader!./nz-demo-modal-locale.component'); + NzDemoModalStyleCode = require('!!raw-loader!./nz-demo-modal-style.component'); + NzDemoConfirmDestroyCode = require('!!raw-loader!./nz-demo-confirm-destroy.component'); + NzDemoModalServiceCode = require('!!raw-loader!./nz-demo-modal-service.component'); +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal.html b/src/showcase/nzx-demo-modal/nz-demo-modal.html new file mode 100644 index 00000000000..164506b8a7b --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal.html @@ -0,0 +1,414 @@ +
+

Modal 对话框

+

模态对话框。

+

何时使用 + +

+

需要用户处理事务,又不希望跳转页面以致打断工作流程时,可以使用 Modal 在当前页面正中打开一个浮层,承载相应的操作。

+

推荐使用加载Component的方式弹出Modal,这样弹出层的Component逻辑可以与外层Component完全隔离,并且做到可以随时复用

+

在弹出层Component中可以通过依赖注入`ModalPublicAgent`方式直接获取模态框的组件实例,用于控制在弹出层组件中控制模态框行为

+

另外当需要一个简洁的确认框询问用户时,可以使用精心封装好的 NzxModalService.confirm() 等方法。

+
+

代码演示

+
+ +
+ + +
+

最简单的用法。

+
+
+ + +
+

Modal的service用法,示例中演示了用户自定义模板、自定义component、以及注入模态框实例的方法。

+
+
注意:如果使用component模式,则需要在NgModule中的 declarations 和 + entryComponents 加入自定义的component +
+
+
+
+ + +
+

使用 confirm() 可以快捷地弹出确认框。onCancel/onOk 返回 promise 可以延迟关闭

+
+
+ + +
+

设置 okTextcancelText 以自定义按钮文字。

+
+
+ + +
+

您可以直接使用 nzStyle.top 或配合其他样式来设置对话框位置。

+
+
注意:如果在Component中没有加入 + encapsulation: ViewEncapsulation.None,因样式隔离,你可能需要在自定义样式时加上 + ::ng-deep来进行样式覆盖。
+
+
+
+
+
+
+ + +
+

点击确定后异步关闭对话框,例如提交表单。

+
+
+ + +
+

更复杂的例子,自定义了页头的内容及页脚的按钮,点击提交后进入 loading 状态,完成后关闭。

+

不需要默认确定取消按钮时,你可以把 footer 设为 null。

+
+
+ + +
+

使用 confirm() 可以快捷地弹出确认框。

+
+
+ + +
+

各种类型的信息提示,只提供一个按钮用于关闭。

+
+
+ + +
+

手动关闭modal。

+
+
+
+
+
+

API + +

+
+

对话框当前分为2种模式,普通模式确认框模式(即Confirm对话框,通过confirm/info/success/error/warning弹出),两种模式对API的支持程度稍有不同。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数说明类型介绍默认值
nzAfterCloseModal 完全关闭后的回调EventEmitter
nzBodyStyleModal body 样式object
nzCancelText取消按钮文字string设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效)取消
nzClosable是否显示右上角的关闭按钮boolean确认框模式下该值无效(默认会被隐藏)true
nzOkLoading确定按钮 loadingbooleanfalse
nzCancelLoading取消按钮 loadingbooleanfalse
nzFooter底部内容string|TemplateRef|ModalButtonOptions1. 仅在普通模式下有效。2. 可通过传入 ModalButtonOptions 来最大程度自定义按钮(详见案例或下方说明)。3. 当不需要底部时,可以设为 null默认的确定取消按钮
nzGetContainer指定 Modal 挂载的 HTML 节点HTMLElement|() => HTMLElementdocument.body
nzMask是否展示遮罩booleantrue
nzMaskClosable点击蒙层是否允许关闭booleantrue
nzMaskStyle遮罩样式object
nzOkText确认按钮文字string设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效)确定
nzOkType确认按钮类型string与button的type类型值一致primary
nzStyle可用于设置浮层的样式,调整浮层位置等object
nzTitle标题string|TemplateRef留空表示不展示标题。TemplateRef的使用方法可参考案例-
nzVisible对话框是否可见boolean当标签方式使用时,请使用双向绑定,例如:[(nzVisible)]="visible"false
nzWidth宽度string|number使用数字时,默认单位为px520
nzWrapClassName对话框外层容器的类名string
nzZIndex设置 Modal 的 z-indexnumber1000
nzOnCancel点击遮罩层或右上角叉或取消按钮的回调EventEmitter注:当以NzModalService.create创建时,此参数应传入function(回调函数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭)
nzOnOk点击确定回调EventEmitter
nzContent内容string|TemplateRef|Component|ng-content
nzComponentParams当nzContent为组件类(Component)时,该参数中的属性将传入nzContent实例中object
nzCancelText取消按钮文字string取消
nzIconType图标 Icon 类型string仅确认框模式支持question-circle
+ +

服务方式创建普通模式对话框 + +

+
+

您可调用NzModalService.create(options)来动态创建普通模式对话框,这里的 options 是一个对象,支持上方API中给出的支持 普通模式 的参数

+
+ +

确认框模式 - NzModalService.method() + +

+

包括:

+
    +
  • NzModalService.info

  • +
  • NzModalService.success

  • +
  • NzModalService.error

  • +
  • NzModalService.warning

  • +
  • NzModalService.confirm

  • +
+

以上均为一个函数,参数为 object,与上方API一致,部分属性类型或初始值有所不同,已列在下方:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数说明类型介绍默认值
nzOnOk点击确定按钮时将执行的回调函数function该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭)
nzOnCancel点击遮罩层或右上角叉或取消按钮的回调function该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭)
nzWidth宽度string or number416
nzMaskClosable点击蒙层是否允许关闭booleanfalse
+

以上函数调用后,会返回一个引用,可以通过该引用关闭弹窗。

+
constructor(modal: NzxModalService) {{ '{' }}
+  const ref: ModalPublicAgent = modal.info();
+  ref.destroy(); // 注:这里将直接销毁对话框
+{{ '}' }}
+

ModalPublicAgent对象(用于控制对话框) + +

+

+ 通过 服务调用方式`NzModalService.xxx()`创建的对话框,都会返回一个`ModalPublicAgent`对象,用于操控该对话框。 +
(若使用nzContent为Component时,也可通过依赖注入`ModalPublicAgent`方式获得此对象),该对象具有以下方法: +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
方法介绍说明
open()打开(显示)对话框若对话框已销毁,则调用此函数将失效
close()关闭(隐藏)对话框注:当用于以服务方式创建的对话框,此方法将直接 销毁 对话框(同destroy方法)
destroy()销毁对话框注:仅用于服务方式创建的对话框(非服务方式创建的对话框,此方法只会隐藏对话框)
+ +

ModalButtonOptions - 自定义底部按钮 + +

+
+

可将此类型数组传入 nzFooter 来自定义按钮

+
+

按钮配置项如下(与button组件保持一致):

+
nzFooter: [{{ '{' }}
+  label: string; // 按钮文本
+  type?: string; // 类型
+  shape?: string; // 形状
+  ghost?: boolean; // 是否ghost
+  size?: string; // 大小
+  autoLoading?: boolean; // 默认为true,若为true时,当onClick返回promise时此按钮将自动置为loading状态
+
+  // 提示:下方方法的this指向该配置对象自身。当nzContent为组件类时,下方方法传入的contentComponentInstance参数为该组件类的实例
+  // 是否显示该按钮
+  show?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean);
+  // 是否显示为loading
+  loading?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean);
+  // 是否禁用
+  disabled?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean);
+  // 按钮点击回调
+  onClick?(this: ModalButtonOptions, contentComponentInstance?: object): void | Promise<void> | any;
+{{ '}' }}]
+

以上配置项也可在运行态实时改变,来触发按钮行为改变

+
+
diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal.module.ts b/src/showcase/nzx-demo-modal/nz-demo-modal.module.ts new file mode 100644 index 00000000000..946ab90cf41 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal.module.ts @@ -0,0 +1,41 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NzCodeBoxModule } from '../share/nz-codebox/nz-codebox.module'; +import { NzDemoConfirmAsyncComponent } from './nz-demo-confirm-async.component'; +import { NzDemoConfirmBasicComponent } from './nz-demo-confirm-basic.component'; +import { NzDemoConfirmDestroyComponent } from './nz-demo-confirm-destroy.component'; +import { NzDemoConfirmInfoComponent } from './nz-demo-confirm-info.component'; +import { NzDemoModalAsyncComponent } from './nz-demo-modal-async.component'; +import { NzDemoModalBasicComponent } from './nz-demo-modal-basic.component'; +import { NzDemoModalCustomizeComponent } from './nz-demo-modal-customize.component'; +import { NzDemoModalLocaleComponent } from './nz-demo-modal-locale.component'; +import { NzDemoModalServiceComponent, NzModalCustomComponent } from './nz-demo-modal-service.component'; +import { NzDemoModalStyleComponent } from './nz-demo-modal-style.component'; +import { NzDemoModalComponent } from './nz-demo-modal.component'; + +import { NgZorroAntdModule } from '../../../index.showcase'; + +import { NzDemoModalRoutingModule } from './nz-demo-modal.routing.module'; + +@NgModule({ + imports: [ NzDemoModalRoutingModule, CommonModule, NzCodeBoxModule, NgZorroAntdModule ], + declarations: [ + NzDemoModalComponent, + NzDemoModalBasicComponent, + NzDemoModalCustomizeComponent, + NzDemoModalAsyncComponent, + NzDemoConfirmBasicComponent, + NzDemoConfirmAsyncComponent, + NzDemoConfirmInfoComponent, + NzDemoModalLocaleComponent, + NzDemoModalStyleComponent, + NzDemoConfirmDestroyComponent, + NzDemoModalServiceComponent, + NzModalCustomComponent + ], + entryComponents: [ NzModalCustomComponent ] +}) +export class NzDemoModalModule { + +} diff --git a/src/showcase/nzx-demo-modal/nz-demo-modal.routing.module.ts b/src/showcase/nzx-demo-modal/nz-demo-modal.routing.module.ts new file mode 100644 index 00000000000..080852f6d19 --- /dev/null +++ b/src/showcase/nzx-demo-modal/nz-demo-modal.routing.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { NzDemoModalComponent } from './nz-demo-modal.component'; + +@NgModule({ + imports: [ RouterModule.forChild([ + { path: '', component: NzDemoModalComponent } + ]) ], + exports: [ RouterModule ] +}) +export class NzDemoModalRoutingModule { +} diff --git a/src/showcase/router.ts b/src/showcase/router.ts index d54f00f2edd..2276887782f 100644 --- a/src/showcase/router.ts +++ b/src/showcase/router.ts @@ -278,6 +278,11 @@ export const ROUTER_LIST = { // 'loadChildren': './nz-demo-modal/nz-demo-modal.module#NzDemoModalModule', 'zh' : '对话框' }, + { + 'label' : 'NzxModal', + 'path' : 'components/nzx-modal', + 'zh' : '对话框 New' + }, { 'label' : 'Notification', 'path' : 'components/notification', @@ -493,6 +498,10 @@ export const DEMO_ROUTES = [ 'path' : 'components/modal', 'loadChildren': './nz-demo-modal/nz-demo-modal.module#NzDemoModalModule' }, + { + 'path' : 'components/nzx-modal', + 'loadChildren': './nzx-demo-modal/nz-demo-modal.module#NzDemoModalModule' + }, { 'path' : 'components/notification', 'loadChildren': './nz-demo-notification/nz-demo-notification.module#NzDemoNotificationModule'