diff --git a/components/popconfirm/demo/async.md b/components/popconfirm/demo/async.md new file mode 100755 index 0000000000..1e32334fd1 --- /dev/null +++ b/components/popconfirm/demo/async.md @@ -0,0 +1,16 @@ +--- +order: 6 +title: + zh-CN: 异步关闭 + en-US: Asynchronously close +--- + +## zh-CN + +点击确定后异步关闭 Popconfirm,例如提交表单。 + +## en-US + +Asynchronously close a popconfirm when a the OK button is pressed. For example, you can use this pattern when you submit a form. + + diff --git a/components/popconfirm/demo/async.ts b/components/popconfirm/demo/async.ts new file mode 100644 index 0000000000..0b88bf3eaa --- /dev/null +++ b/components/popconfirm/demo/async.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { NzMessageService } from 'ng-zorro-antd/message'; + +@Component({ + selector: 'nz-demo-popconfirm-async', + template: ` + + ` +}) +export class NzDemoPopconfirmAsyncComponent { + cancel(): void { + this.nzMessageService.info('click cancel'); + } + + confirm(): void { + this.nzMessageService.info('click confirm'); + } + + beforeConfirm(): Observable { + return new Observable(observer => { + setTimeout(() => { + observer.next(true); + observer.complete(); + }, 3000); + }); + } + + constructor(private nzMessageService: NzMessageService) {} +} diff --git a/components/popconfirm/demo/promise.md b/components/popconfirm/demo/promise.md new file mode 100755 index 0000000000..5e54a74b95 --- /dev/null +++ b/components/popconfirm/demo/promise.md @@ -0,0 +1,16 @@ +--- +order: 7 +title: + zh-CN: 基于 Promise 的异步关闭 + en-US: Asynchronously close on Promise +--- + +## zh-CN + +点击确定后异步关闭 Popconfirm,例如提交表单。 + +## en-US + +Asynchronously close a popconfirm when the OK button is pressed. For example, you can use this pattern when you submit a form. + + diff --git a/components/popconfirm/demo/promise.ts b/components/popconfirm/demo/promise.ts new file mode 100644 index 0000000000..24fc130db4 --- /dev/null +++ b/components/popconfirm/demo/promise.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; + +import { NzMessageService } from 'ng-zorro-antd/message'; + +@Component({ + selector: 'nz-demo-popconfirm-promise', + template: ` + + ` +}) +export class NzDemoPopconfirmPromiseComponent { + cancel(): void { + this.nzMessageService.info('click cancel'); + } + + confirm(): void { + this.nzMessageService.info('click confirm'); + } + + beforeConfirm(): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, 3000); + }); + } + + constructor(private nzMessageService: NzMessageService) {} +} diff --git a/components/popconfirm/doc/index.en-US.md b/components/popconfirm/doc/index.en-US.md index d92944721d..feec99d0a2 100644 --- a/components/popconfirm/doc/index.en-US.md +++ b/components/popconfirm/doc/index.en-US.md @@ -46,6 +46,7 @@ import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm'; | `[nzCondition]` | Whether to directly emit `onConfirm` without showing Popconfirm | `boolean` | `false` | - | | `[nzIcon]` | Customize icon of confirmation | `string \| TemplateRef` | - | - | | `[nzAutoFocus]` | Autofocus a button | `null \| 'ok' \| 'cancel'` | `null` | ✅ | +| `[nzBeforeConfirm]` | The hook before the confirmation operation, decides whether to continue responding to the `nzOnConfirm` callback, supports asynchronous verification. | `(() => Observable \| Promise \| boolean) \| null` | `null` | - | | `(nzOnCancel)` | Callback of cancel | `EventEmitter` | - | - | | `(nzOnConfirm)` | Callback of confirmation | `EventEmitter` | - | - | diff --git a/components/popconfirm/doc/index.zh-CN.md b/components/popconfirm/doc/index.zh-CN.md index 1c20c08d01..d20bb041d4 100644 --- a/components/popconfirm/doc/index.zh-CN.md +++ b/components/popconfirm/doc/index.zh-CN.md @@ -50,6 +50,7 @@ import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm'; | `[nzCondition]` | 是否直接触发 `nzOnConfirm` 而不弹出框 | `boolean` | `false` | - | | `[nzIcon]` | 自定义弹出框的 icon | `string \| TemplateRef` | - | - | | `[nzAutoFocus]` | 按钮的自动聚焦 | `null \| 'ok' \| 'cancel'` | `null` | ✅ | +| `[nzBeforeConfirm]` | 确认操作之前的钩子,决定是否继续响应 `nzOnConfirm` 回调,支持异步验证。 | `(() => Observable \| Promise \| boolean) \| null` | `null` | - | | `(nzOnCancel)` | 点击取消的回调 | `EventEmitter` | - | - | | `(nzOnConfirm)` | 点击确认的回调 | `EventEmitter` | - | - | diff --git a/components/popconfirm/popconfirm.spec.ts b/components/popconfirm/popconfirm.spec.ts index 04796f8b52..d27f596d9a 100644 --- a/components/popconfirm/popconfirm.spec.ts +++ b/components/popconfirm/popconfirm.spec.ts @@ -3,6 +3,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { ComponentFixture, fakeAsync, inject, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Observable } from 'rxjs'; import { NzButtonType } from 'ng-zorro-antd/button'; import { dispatchMouseEvent } from 'ng-zorro-antd/core/testing'; @@ -144,6 +145,74 @@ describe('NzPopconfirm', () => { expect(component.cancel).toHaveBeenCalledTimes(0); })); + it('should before confirm work', fakeAsync(() => { + const triggerElement = component.stringTemplate.nativeElement; + + dispatchMouseEvent(triggerElement, 'click'); + fixture.detectChanges(); + expect(getTitleText()!.textContent).toContain('title-string'); + expect(component.confirm).toHaveBeenCalledTimes(0); + expect(component.cancel).toHaveBeenCalledTimes(0); + + component.beforeConfirm = () => false; + fixture.detectChanges(); + + dispatchMouseEvent(getTooltipTrigger(1), 'click'); + waitingForTooltipToggling(); + expect(component.confirm).toHaveBeenCalledTimes(0); + expect(component.cancel).toHaveBeenCalledTimes(0); + expect(getTitleText()!.textContent).toContain('title-string'); + })); + + it('should before confirm observable work', fakeAsync(() => { + const triggerElement = component.stringTemplate.nativeElement; + + dispatchMouseEvent(triggerElement, 'click'); + fixture.detectChanges(); + expect(getTitleText()!.textContent).toContain('title-string'); + expect(component.confirm).toHaveBeenCalledTimes(0); + expect(component.cancel).toHaveBeenCalledTimes(0); + + component.beforeConfirm = () => + new Observable(observer => { + setTimeout(() => { + observer.next(true); + observer.complete(); + }, 200); + }); + + dispatchMouseEvent(getTooltipTrigger(1), 'click'); + tick(200 + 10); + waitingForTooltipToggling(); + expect(getTitleText()).toBeNull(); + expect(component.confirm).toHaveBeenCalledTimes(1); + expect(component.cancel).toHaveBeenCalledTimes(0); + })); + + it('should before confirm promise work', fakeAsync(() => { + const triggerElement = component.stringTemplate.nativeElement; + + dispatchMouseEvent(triggerElement, 'click'); + fixture.detectChanges(); + expect(getTitleText()!.textContent).toContain('title-string'); + expect(component.confirm).toHaveBeenCalledTimes(0); + expect(component.cancel).toHaveBeenCalledTimes(0); + + component.beforeConfirm = () => + new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, 200); + }); + + dispatchMouseEvent(getTooltipTrigger(1), 'click'); + tick(200 + 10); + waitingForTooltipToggling(); + expect(getTitleText()).toBeNull(); + expect(component.confirm).toHaveBeenCalledTimes(1); + expect(component.cancel).toHaveBeenCalledTimes(0); + })); + it('should nzPopconfirmShowArrow work', fakeAsync(() => { const triggerElement = component.stringTemplate.nativeElement; dispatchMouseEvent(triggerElement, 'click'); @@ -178,6 +247,7 @@ describe('NzPopconfirm', () => { nzCancelText="cancel-text" [nzAutofocus]="autoFocus" [nzCondition]="condition" + [nzBeforeConfirm]="beforeConfirm" [nzPopconfirmShowArrow]="nzPopconfirmShowArrow" [nzPopconfirmBackdrop]="nzPopconfirmBackdrop" (nzOnConfirm)="confirm()" @@ -210,6 +280,7 @@ export class NzPopconfirmTestNewComponent { icon: string | undefined = undefined; nzPopconfirmBackdrop = false; autoFocus: NzAutoFocusType = null; + beforeConfirm: (() => Observable | Promise | boolean) | null = null; @ViewChild('stringTemplate', { static: false }) stringTemplate!: ElementRef; @ViewChild('templateTemplate', { static: false }) templateTemplate!: ElementRef; diff --git a/components/popconfirm/popconfirm.ts b/components/popconfirm/popconfirm.ts index aaf8ecca52..f93215dbd4 100644 --- a/components/popconfirm/popconfirm.ts +++ b/components/popconfirm/popconfirm.ts @@ -27,15 +27,15 @@ import { ViewContainerRef, ViewEncapsulation } from '@angular/core'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Observable, Subject } from 'rxjs'; +import { finalize, first, takeUntil } from 'rxjs/operators'; import { NzButtonType } from 'ng-zorro-antd/button'; import { zoomBigMotion } from 'ng-zorro-antd/core/animation'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation'; import { BooleanInput, NgStyleInterface, NzSafeAny, NzTSType } from 'ng-zorro-antd/core/types'; -import { InputBoolean } from 'ng-zorro-antd/core/util'; +import { InputBoolean, wrapIntoObservable } from 'ng-zorro-antd/core/util'; import { NzTooltipBaseDirective, NzToolTipComponent, NzTooltipTrigger, PropertyMapping } from 'ng-zorro-antd/tooltip'; export type NzAutoFocusType = null | 'ok' | 'cancel'; @@ -70,6 +70,7 @@ export class NzPopconfirmDirective extends NzTooltipBaseDirective { @Input() nzOkType?: string; @Input() nzOkDanger?: boolean; @Input() nzCancelText?: string; + @Input() nzBeforeConfirm?: () => Observable | Promise | boolean; @Input() nzIcon?: string | TemplateRef; @Input() @InputBoolean() nzCondition: boolean = false; @Input() @InputBoolean() nzPopconfirmShowArrow: boolean = true; @@ -90,6 +91,7 @@ export class NzPopconfirmDirective extends NzTooltipBaseDirective { nzOkType: ['nzOkType', () => this.nzOkType], nzOkDanger: ['nzOkDanger', () => this.nzOkDanger], nzCancelText: ['nzCancelText', () => this.nzCancelText], + nzBeforeConfirm: ['nzBeforeConfirm', () => this.nzBeforeConfirm], nzCondition: ['nzCondition', () => this.nzCondition], nzIcon: ['nzIcon', () => this.nzIcon], nzPopconfirmShowArrow: ['nzPopconfirmShowArrow', () => this.nzPopconfirmShowArrow], @@ -190,6 +192,7 @@ export class NzPopconfirmDirective extends NzTooltipBaseDirective { [nzSize]="'small'" [nzType]="nzOkType !== 'danger' ? nzOkType : 'primary'" [nzDanger]="nzOkDanger || nzOkType === 'danger'" + [nzLoading]="confirmLoading" (click)="onConfirm()" [attr.cdkFocusInitial]="nzAutoFocus === 'ok' || null" > @@ -217,6 +220,7 @@ export class NzPopconfirmComponent extends NzToolTipComponent implements OnDestr nzOkType: NzButtonType | 'danger' = 'primary'; nzOkDanger: boolean = false; nzAutoFocus: NzAutoFocusType = null; + nzBeforeConfirm: (() => Observable | Promise | boolean) | null = null; readonly nzOnCancel = new Subject(); readonly nzOnConfirm = new Subject(); @@ -227,6 +231,8 @@ export class NzPopconfirmComponent extends NzToolTipComponent implements OnDestr override _prefix = 'ant-popover'; + confirmLoading = false; + constructor( cdr: ChangeDetectorRef, private elementRef: ElementRef, @@ -262,14 +268,37 @@ export class NzPopconfirmComponent extends NzToolTipComponent implements OnDestr this.restoreFocus(); } + handleConfirm(): void { + this.nzOnConfirm.next(); + super.hide(); + } + onCancel(): void { this.nzOnCancel.next(); super.hide(); } onConfirm(): void { - this.nzOnConfirm.next(); - super.hide(); + if (this.nzBeforeConfirm) { + const observable = wrapIntoObservable(this.nzBeforeConfirm()).pipe(first()); + this.confirmLoading = true; + observable + .pipe( + finalize(() => { + this.confirmLoading = false; + this.cdr.markForCheck(); + }), + takeUntil(this.nzVisibleChange), + takeUntil(this.destroy$) + ) + .subscribe(value => { + if (value) { + this.handleConfirm(); + } + }); + } else { + this.handleConfirm(); + } } private capturePreviouslyFocusedElement(): void {