Skip to content

Commit

Permalink
feat(module:tabs): add nzCanDeactivate hook (#4476)
Browse files Browse the repository at this point in the history
* feat(module:tabs): add  and  hooks

* feat(module:tabs): fix bug with onpush

* feat(module:tabs): remove canDeactivate and add test

* feat(module:tabs): update demo

* feat(module:tabs): modify api name

* feat(module:tabs): remove console

close #4432
  • Loading branch information
danranVm authored and hsuanxyz committed Jan 8, 2020
1 parent cc8018a commit a533980
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 13 deletions.
23 changes: 23 additions & 0 deletions components/core/util/observable.ts
@@ -0,0 +1,23 @@
/**
* @license
* Copyright Alibaba.com All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
*/

import { from, isObservable, Observable, of } from 'rxjs';
import { isPromise } from './is-promise';

export function wrapIntoObservable<T>(value: T | Promise<T> | Observable<T>): Observable<T> {
if (isObservable(value)) {
return value;
}

if (isPromise(value)) {
// Use `Promise.resolve()` to wrap promise-like instances.
return from(Promise.resolve(value));
}

return of(value);
}
1 change: 1 addition & 0 deletions components/core/util/public-api.ts
Expand Up @@ -21,3 +21,4 @@ export * from './text-measure';
export * from './measure-scrollbar';
export * from './ensure-in-bounds';
export * from './tick';
export * from './observable';
2 changes: 1 addition & 1 deletion components/notification/nz-notification.component.html
Expand Up @@ -51,7 +51,7 @@
</ng-template>
<a tabindex="0" class="ant-notification-notice-close" (click)="close()">
<span class="ant-notification-notice-close-x">
<ng-container *ngIf="nzMessage.options?.nzCloseIcon else iconTpl">
<ng-container *ngIf="nzMessage.options?.nzCloseIcon; else iconTpl">
<ng-container *nzStringTemplateOutlet="nzMessage.options?.nzCloseIcon">
<i nz-icon [nzType]="nzMessage.options?.nzCloseIcon"></i>
</ng-container>
Expand Down
14 changes: 14 additions & 0 deletions components/tabs/demo/guard.md
@@ -0,0 +1,14 @@
---
order: 14
title:
zh-CN: 标签守卫
en-US: Tab guard
---

## zh-CN

通过 `nzCanDeactivate` 决定一个 tab 是否可以被切换。

## en-US

Via `nzCanDeactivate` to determine if a tab can be deactivated.
47 changes: 47 additions & 0 deletions components/tabs/demo/guard.ts
@@ -0,0 +1,47 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzTabsCanDeactivateFn } from 'ng-zorro-antd/tabs';
import { Observable } from 'rxjs';

@Component({
selector: 'nz-demo-tabs-guard',
template: `
<nz-tabset [nzCanDeactivate]="canDeactivate">
<nz-tab *ngFor="let tab of tabs" [nzTitle]="'Tab' + tab"> Content of tab {{ tab }} </nz-tab>
</nz-tabset>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NzDemoTabsGuardComponent {
tabs = [1, 2, 3, 4];
constructor(private modal: NzModalService) {}

canDeactivate: NzTabsCanDeactivateFn = (fromIndex: number, toIndex: number) => {
switch (fromIndex) {
case 0:
return toIndex === 1;
case 1:
return Promise.resolve(toIndex === 2);
case 2:
return this.confirm();
default:
return true;
}
};

private confirm(): Observable<boolean> {
return new Observable(observer => {
this.modal.confirm({
nzTitle: 'Are you sure you want to leave this tab?',
nzOnOk: () => {
observer.next(true);
observer.complete();
},
nzOnCancel: () => {
observer.next(false);
observer.complete();
}
});
});
}
}
3 changes: 2 additions & 1 deletion components/tabs/demo/module
Expand Up @@ -4,5 +4,6 @@ import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzInputNumberModule } from 'ng-zorro-antd/input-number';
import { NzModalModule } from 'ng-zorro-antd/modal';

export const moduleList = [ NzTabsModule, NzIconModule, NzSelectModule, NzRadioModule, NzInputNumberModule, NzButtonModule ];
export const moduleList = [ NzTabsModule, NzIconModule, NzSelectModule, NzRadioModule, NzInputNumberModule, NzButtonModule, NzModalModule ];
1 change: 1 addition & 0 deletions components/tabs/doc/index.en-US.md
Expand Up @@ -37,6 +37,7 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs';
| `[nzShowPagination]` | Whether show pre or next button when exceed display area | `boolean` | `true` ||
| `[nzLinkRouter]` | Link with Angular router. It supports child mode and query param mode | `boolean` | `false` ||
| `[nzLinkExact]` | Use exact routing matching | `boolean` | `true` |
| `[nzCanDeactivate]` | Determine if a tab can be deactivated | `NzTabsCanDeactivateFn` | - |
| `(nzSelectedIndexChange)` | Current tab's index change callback | `EventEmitter<number>` | - |
| `(nzSelectChange)` | Current tab's change callback | `EventEmitter<{nzSelectedIndex: number,tab: NzTabComponent}>` | - |
| `(nzOnNextClick)` | Callback executed when next button is clicked | `EventEmitter<void>` | - |
Expand Down
1 change: 1 addition & 0 deletions components/tabs/doc/index.zh-CN.md
Expand Up @@ -40,6 +40,7 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs';
| `[nzShowPagination]` | 是否超出范围时显示pre和next按钮 | `boolean` | `true` ||
| `[nzLinkRouter]` | 与 Angular 路由联动 | `boolean` | `false` ||
| `[nzLinkExact]` | 以严格匹配模式确定联动的路由 | `boolean` | `true` |
| `[nzCanDeactivate]` | 决定一个 tab 是否可以被切换 | `NzTabsCanDeactivateFn` | - |
| `(nzSelectedIndexChange)` | 当前激活 tab 面板的 序列号变更回调函数 | `EventEmitter<number>` | - |
| `(nzSelectChange)` | 当前激活 tab 面板变更回调函数 | `EventEmitter<{nzSelectedIndex: number,tab: NzTabComponent}>` | - |
| `(nzOnNextClick)` | next 按钮被点击的回调 | `EventEmitter<void>` | - |
Expand Down
126 changes: 123 additions & 3 deletions components/tabs/nz-tabs.spec.ts
Expand Up @@ -7,8 +7,9 @@ import { RouterTestingModule } from '@angular/router/testing';

import { NgStyleInterface } from 'ng-zorro-antd/core';

import { of } from 'rxjs';
import { NzTabsModule } from './nz-tabs.module';
import { NzAnimatedInterface, NzTabSetComponent } from './nz-tabset.component';
import { NzAnimatedInterface, NzTabsCanDeactivateFn, NzTabSetComponent } from './nz-tabset.component';

describe('tabs', () => {
beforeEach(fakeAsync(() => {
Expand Down Expand Up @@ -467,6 +468,122 @@ describe('tabs', () => {
expect(testComponent.select02).toHaveBeenCalledTimes(0);
expect(testComponent.deselect02).toHaveBeenCalledTimes(0);
}));

it('should switch hook work', fakeAsync(() => {
fixture.detectChanges();
tick();
fixture.detectChanges();
testComponent.add = true;
// O => 1 => 2 => 0
testComponent.canDeactivate = (fromIndex: number, toIndex: number) => {
switch (fromIndex) {
case 0:
return toIndex === 1;
case 1:
return Promise.resolve(toIndex === 2);
case 2:
return of(toIndex === 0);
default:
return true;
}
};
fixture.detectChanges();
tick();
fixture.detectChanges();

const titles = tabs.nativeElement.querySelectorAll('.ant-tabs-tab');
// 0 => 2: not
titles[2].click();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(testComponent.selectedIndex).toBe(0);
expect(testComponent.selectedIndexChange).toHaveBeenCalledTimes(0);
expect(testComponent.selectChange).toHaveBeenCalledTimes(0);
expect(testComponent.click00).toHaveBeenCalledTimes(0);
expect(testComponent.select00).toHaveBeenCalledTimes(0);
expect(testComponent.deselect00).toHaveBeenCalledTimes(0);
expect(testComponent.click02).toHaveBeenCalledTimes(0);
expect(testComponent.select02).toHaveBeenCalledTimes(0);
expect(testComponent.deselect02).toHaveBeenCalledTimes(0);

// 0 => 1: yes
titles[1].click();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(testComponent.selectedIndex).toBe(1);
expect(testComponent.selectedIndexChange).toHaveBeenCalledTimes(1);
expect(testComponent.selectChange).toHaveBeenCalledTimes(1);
expect(testComponent.click00).toHaveBeenCalledTimes(0);
expect(testComponent.select00).toHaveBeenCalledTimes(0);
expect(testComponent.deselect00).toHaveBeenCalledTimes(1);
expect(testComponent.click01).toHaveBeenCalledTimes(1);
expect(testComponent.select01).toHaveBeenCalledTimes(1);
expect(testComponent.deselect01).toHaveBeenCalledTimes(0);

// 1 => 0: not
titles[0].click();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(testComponent.selectedIndex).toBe(1);
expect(testComponent.selectedIndexChange).toHaveBeenCalledTimes(1);
expect(testComponent.selectChange).toHaveBeenCalledTimes(1);
expect(testComponent.click01).toHaveBeenCalledTimes(1);
expect(testComponent.select01).toHaveBeenCalledTimes(1);
expect(testComponent.deselect01).toHaveBeenCalledTimes(0);
expect(testComponent.click00).toHaveBeenCalledTimes(0);
expect(testComponent.select00).toHaveBeenCalledTimes(0);
expect(testComponent.deselect00).toHaveBeenCalledTimes(1);

// 1 => 2: yes
titles[2].click();
fixture.detectChanges();
tick();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(testComponent.selectedIndex).toBe(2);
expect(testComponent.selectedIndexChange).toHaveBeenCalledTimes(2);
expect(testComponent.selectChange).toHaveBeenCalledTimes(2);
expect(testComponent.click01).toHaveBeenCalledTimes(1);
expect(testComponent.select01).toHaveBeenCalledTimes(1);
expect(testComponent.deselect01).toHaveBeenCalledTimes(1);
expect(testComponent.click02).toHaveBeenCalledTimes(1);
expect(testComponent.select02).toHaveBeenCalledTimes(1);
expect(testComponent.deselect02).toHaveBeenCalledTimes(1);

// 2 => 1: not
titles[1].click();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(testComponent.selectedIndex).toBe(2);
expect(testComponent.selectedIndexChange).toHaveBeenCalledTimes(2);
expect(testComponent.selectChange).toHaveBeenCalledTimes(2);
expect(testComponent.click02).toHaveBeenCalledTimes(1);
expect(testComponent.select02).toHaveBeenCalledTimes(1);
expect(testComponent.deselect02).toHaveBeenCalledTimes(1);
expect(testComponent.click01).toHaveBeenCalledTimes(1);
expect(testComponent.select01).toHaveBeenCalledTimes(1);
expect(testComponent.deselect01).toHaveBeenCalledTimes(1);

// 2 => 0: yes
titles[0].click();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(testComponent.selectedIndex).toBe(0);
expect(testComponent.selectedIndexChange).toHaveBeenCalledTimes(3);
expect(testComponent.selectChange).toHaveBeenCalledTimes(3);
expect(testComponent.click02).toHaveBeenCalledTimes(1);
expect(testComponent.select02).toHaveBeenCalledTimes(1);
expect(testComponent.deselect02).toHaveBeenCalledTimes(2);
expect(testComponent.click00).toHaveBeenCalledTimes(1);
expect(testComponent.select00).toHaveBeenCalledTimes(1);
expect(testComponent.deselect00).toHaveBeenCalledTimes(2);
}));
});

describe('init nzTabPosition to left', () => {
Expand Down Expand Up @@ -561,6 +678,7 @@ describe('link router', () => {
[nzType]="type"
[nzTabBarGutter]="tabBarGutter"
[nzHideAll]="hideAll"
[nzCanDeactivate]="canDeactivate"
>
<nz-tab nzTitle="title" [nzForceRender]="true" (nzDeselect)="deselect00()" (nzSelect)="select00()" (nzClick)="click00()"
>Content 1<!----></nz-tab
Expand Down Expand Up @@ -589,8 +707,8 @@ export class NzTestTabsBasicComponent {
@ViewChild('extraTemplate', { static: false }) extraTemplate: TemplateRef<void>;
@ViewChild(NzTabSetComponent, { static: false }) nzTabSetComponent: NzTabSetComponent;
selectedIndex = 0;
selectedIndexChange = jasmine.createSpy('selectedIndex callback');
selectChange = jasmine.createSpy('selectedIndex callback');
selectedIndexChange = jasmine.createSpy('selectedIndexChange callback');
selectChange = jasmine.createSpy('selectChange callback');
animated: NzAnimatedInterface | boolean = true;
size = 'default';
tabBarExtraContent: TemplateRef<void>;
Expand All @@ -612,6 +730,8 @@ export class NzTestTabsBasicComponent {
select02 = jasmine.createSpy('select02 callback');
deselect02 = jasmine.createSpy('deselect02 callback');
array = [];

canDeactivate: NzTabsCanDeactivateFn | null = null;
}

/** https://github.com/NG-ZORRO/ng-zorro-antd/issues/1964 **/
Expand Down
26 changes: 20 additions & 6 deletions components/tabs/nz-tabset.component.ts
Expand Up @@ -32,7 +32,7 @@ import {
ViewEncapsulation
} from '@angular/core';
import { NavigationEnd, Router, RouterLink, RouterLinkWithHref } from '@angular/router';
import { merge, Subject, Subscription } from 'rxjs';
import { merge, Observable, Subject, Subscription } from 'rxjs';

import {
InputBoolean,
Expand All @@ -42,9 +42,10 @@ import {
NzUpdateHostClassService,
PREFIX,
toNumber,
WithConfig
WithConfig,
wrapIntoObservable
} from 'ng-zorro-antd/core';
import { filter, startWith, takeUntil } from 'rxjs/operators';
import { filter, first, startWith, takeUntil } from 'rxjs/operators';

import { NzTabComponent } from './nz-tab.component';
import { NzTabsNavComponent } from './nz-tabs-nav.component';
Expand All @@ -59,6 +60,8 @@ export class NzTabChangeEvent {
tab: NzTabComponent;
}

export type NzTabsCanDeactivateFn = (fromIndex: number, toIndex: number) => Observable<boolean> | Promise<boolean> | boolean;

export type NzTabPosition = NzFourDirectionType;
export type NzTabPositionMode = 'horizontal' | 'vertical';
export type NzTabType = 'line' | 'card';
Expand Down Expand Up @@ -109,6 +112,7 @@ export class NzTabSetComponent implements AfterContentChecked, OnInit, AfterView

@Input() @InputBoolean() nzLinkRouter = false;
@Input() @InputBoolean() nzLinkExact = true;
@Input() nzCanDeactivate: NzTabsCanDeactivateFn | null = null;

@Output() readonly nzOnNextClick = new EventEmitter<void>();
@Output() readonly nzOnPrevClick = new EventEmitter<void>();
Expand Down Expand Up @@ -156,12 +160,22 @@ export class NzTabSetComponent implements AfterContentChecked, OnInit, AfterView

clickLabel(index: number, disabled: boolean): void {
if (!disabled) {
const tabs = this.listOfNzTabComponent.toArray();
this.nzSelectedIndex = index;
tabs[index].nzClick.emit();
if (this.nzSelectedIndex !== null && this.nzSelectedIndex !== index && typeof this.nzCanDeactivate === 'function') {
const observable = wrapIntoObservable(this.nzCanDeactivate(this.nzSelectedIndex, index));
observable.pipe(first(), takeUntil(this.destroy$)).subscribe(canChange => canChange && this.emitClickEvent(index));
} else {
this.emitClickEvent(index);
}
}
}

private emitClickEvent(index: number): void {
const tabs = this.listOfNzTabComponent.toArray();
this.nzSelectedIndex = index;
tabs[index].nzClick.emit();
this.cdr.markForCheck();
}

createChangeEvent(index: number): NzTabChangeEvent {
const event = new NzTabChangeEvent();
event.index = index;
Expand Down
2 changes: 1 addition & 1 deletion docs/recommendation.zh-CN.md
Expand Up @@ -15,7 +15,7 @@ title: 资源推荐
工具库|[Component Dev Kit](https://material.angular.io/cdk/categories) | Angular 官方的提供的组件库工具,包含拖拽、浮层、虚拟滚动等大量功能
可视化|[NGX-CHARTS](https://swimlane.github.io/ngx-charts/) | 基于 D3 的Angular 可视化组件库
可视化|[NGX-CHARTS-DAG](https://swimlane.github.io/ngx-graph/) | 基于 Dagre 的有向无环图可视化组件库
无线端|[NG-ZORRO-MOBILE](http://ng.mobile.ant.design/) | Ant Design Mobile 设计规范的 Angular 实现
无线端|[NG-ZORRO-MOBILE](https://ng.mobile.ant.design/) | Ant Design Mobile 设计规范的 Angular 实现
打包 |[Angular CLI](https://cli.angular.io/) | Angular 的配套打包工具
服务端渲染|[Angular Universal](https://universal.angular.io/) | Angular服务端渲染工具

Expand Down
2 changes: 1 addition & 1 deletion scripts/site/_site/doc/app/app.component.html
Expand Up @@ -118,7 +118,7 @@
<div class="footer-center">
<h2>{{ language === 'zh' ? '相关资源' : 'Resources' }}</h2>
<div>
<a href="http://ng.mobile.ant.design" target="_blank" rel="noopener noreferrer">NG-ZORRO-MOBILE</a>
<a href="https://ng.mobile.ant.design" target="_blank" rel="noopener noreferrer">NG-ZORRO-MOBILE</a>
<span> - </span>
<span>Angular</span>
</div>
Expand Down

0 comments on commit a533980

Please sign in to comment.