From f6e965367b1b9acf70524f046fb080647a0b6af9 Mon Sep 17 00:00:00 2001 From: Wendell Date: Thu, 18 Jul 2019 20:04:50 +0800 Subject: [PATCH] feat(module:tabs): support link router (#3718) --- .../nz-descriptions.component.html | 3 +- components/tabs/demo/link-router.md | 14 +++ components/tabs/demo/link-router.ts | 26 +++++ components/tabs/doc/index.en-US.md | 11 +- components/tabs/doc/index.zh-CN.md | 12 ++- components/tabs/nz-tab-link.directive.ts | 24 +++++ components/tabs/nz-tab.component.html | 5 +- components/tabs/nz-tab.component.ts | 7 +- components/tabs/nz-tabs.module.ts | 7 +- components/tabs/nz-tabs.spec.ts | 101 +++++++++++++++++- components/tabs/nz-tabset.component.html | 2 +- components/tabs/nz-tabset.component.ts | 60 ++++++++++- components/tabs/public-api.ts | 1 + components/tabs/style/entry.less | 1 + components/tabs/style/patch.less | 12 +++ scripts/site/_site/doc/app/app.component.ts | 6 +- scripts/site/_site/doc/app/app.module.ts | 2 +- 17 files changed, 278 insertions(+), 16 deletions(-) create mode 100644 components/tabs/demo/link-router.md create mode 100644 components/tabs/demo/link-router.ts create mode 100644 components/tabs/nz-tab-link.directive.ts create mode 100644 components/tabs/style/patch.less diff --git a/components/descriptions/nz-descriptions.component.html b/components/descriptions/nz-descriptions.component.html index f5fb5a0df45..c94004b4705 100644 --- a/components/descriptions/nz-descriptions.component.html +++ b/components/descriptions/nz-descriptions.component.html @@ -1,4 +1,5 @@ -
+
{{ nzTitle }}
diff --git a/components/tabs/demo/link-router.md b/components/tabs/demo/link-router.md new file mode 100644 index 00000000000..209adca6a52 --- /dev/null +++ b/components/tabs/demo/link-router.md @@ -0,0 +1,14 @@ +--- +order: 13 +title: + zh-CN: 路由联动 + en-US: With Router +--- + +## zh-CN + +与路由联动,点击 tab 更改路由,并且在路由改变时自动切换 tab。 + +## en-US + +Link with router. diff --git a/components/tabs/demo/link-router.ts b/components/tabs/demo/link-router.ts new file mode 100644 index 00000000000..13165870780 --- /dev/null +++ b/components/tabs/demo/link-router.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-tabs-link-router', + template: ` + + + Default + Default. + + + Two + Two. + + + Three + Three. + + + Four + Four. + + + ` +}) +export class NzDemoTabsLinkRouterComponent {} diff --git a/components/tabs/doc/index.en-US.md b/components/tabs/doc/index.en-US.md index bbe5e98ab0a..32eb29e559a 100644 --- a/components/tabs/doc/index.en-US.md +++ b/components/tabs/doc/index.en-US.md @@ -39,6 +39,7 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs'; | `[nzTabBarGutter]` | The gap between tabs | `number` | - | | `[nzHideAll]` | Whether hide all tabs | `boolean` | `false` | | `[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` || | `(nzSelectedIndexChange)` | Current tab's index change callback | `EventEmitter` | - | | `(nzSelectChange)` | Current tab's change callback | `EventEmitter<{nzSelectedIndex: number,tab: NzTabComponent}>` | - | | `(nzOnNextClick)` | Callback executed when next button is clicked | `EventEmitter` | - | @@ -57,4 +58,12 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs'; ### [nz-tab] -Tab contents can be lazy loaded by declaring the body in a `ng-template` with the `[nz-tab]` attribute. \ No newline at end of file +Tab contents can be lazy loaded by declaring the body in a `ng-template` with the `[nz-tab]` attribute. + +### nz-tab-link + +Show a link in tab's head. Used in router link mode. + +### Link Router + +This make the tabs component changes `nzSelectedIndex` with Angular route. You must use `nz-tab-link` instead of `[nzTitle]` in this situation. diff --git a/components/tabs/doc/index.zh-CN.md b/components/tabs/doc/index.zh-CN.md index f7c516aa545..0699052545c 100644 --- a/components/tabs/doc/index.zh-CN.md +++ b/components/tabs/doc/index.zh-CN.md @@ -42,6 +42,7 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs'; | `[nzTabBarGutter]` | tabs 之间的间隙 | `number` | - | | `[nzHideAll]` | 是否隐藏所有tab内容 | `boolean` | `false` | | `[nzShowPagination]` | 是否超出范围时显示pre和next按钮 | `boolean` | `true` | +| `[nzLinkRouter]` | 与 Angular 路由联动 | `boolean` | `false` || | `(nzSelectedIndexChange)` | 当前激活 tab 面板的 序列号变更回调函数 | `EventEmitter` | - | | `(nzSelectChange)` | 当前激活 tab 面板变更回调函数 | `EventEmitter<{nzSelectedIndex: number,tab: NzTabComponent}>` | - | | `(nzOnNextClick)` | next 按钮被点击的回调 | `EventEmitter` | - | @@ -58,7 +59,14 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs'; | `(nzSelect)` | tab被选中的回调函数 | `EventEmitter` | - | | `(nzDeselect)` | tab被取消选中的回调函数 | `EventEmitter` | - | - ### [nz-tab] -与 `ng-template` 一同使用,用于标记需要懒加载的 `tab` 内容,具体用法见示例。 \ No newline at end of file +与 `ng-template` 一同使用,用于标记需要懒加载的 `tab` 内容,具体用法见示例。 + +### [nz-tab-link] + +选项卡头显示链接,在路由联动模式下使用。 + +### 路由联动 + +路由联动可以让 tab 的切换和路由行为相一致。使用此功能时,title 必须通过 `nz-tab-link` 组件指定。 diff --git a/components/tabs/nz-tab-link.directive.ts b/components/tabs/nz-tab-link.directive.ts new file mode 100644 index 00000000000..167375465db --- /dev/null +++ b/components/tabs/nz-tab-link.directive.ts @@ -0,0 +1,24 @@ +/** + * @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 { Directive, Optional, Self } from '@angular/core'; +import { RouterLink, RouterLinkWithHref } from '@angular/router'; + +/** + * This component is for catching `routerLink` directive. + */ +@Directive({ + selector: 'a[nz-tab-link]', + exportAs: 'nzTabLink' +}) +export class NzTabLinkDirective { + constructor( + @Optional() @Self() public routerLink?: RouterLink, + @Optional() @Self() public routerLinkWithHref?: RouterLinkWithHref + ) {} +} diff --git a/components/tabs/nz-tab.component.html b/components/tabs/nz-tab.component.html index 5cbbdb3c260..4f330263752 100644 --- a/components/tabs/nz-tab.component.html +++ b/components/tabs/nz-tab.component.html @@ -1,3 +1,6 @@ - + + + + \ No newline at end of file diff --git a/components/tabs/nz-tab.component.ts b/components/tabs/nz-tab.component.ts index 99d795ef107..608353ce7db 100644 --- a/components/tabs/nz-tab.component.ts +++ b/components/tabs/nz-tab.component.ts @@ -25,6 +25,8 @@ import { import { Subject } from 'rxjs'; import { InputBoolean } from 'ng-zorro-antd/core'; + +import { NzTabLinkDirective } from './nz-tab-link.directive'; import { NzTabDirective } from './nz-tab.directive'; @Component({ @@ -40,9 +42,12 @@ export class NzTabComponent implements OnChanges, OnDestroy { origin: number | null = null; isActive = false; readonly stateChanges = new Subject(); - @ViewChild(TemplateRef, { static: true }) content: TemplateRef; + @ViewChild('bodyTpl', { static: true }) content: TemplateRef; + @ViewChild('titleTpl', { static: true }) title: TemplateRef; @ContentChild(NzTabDirective, { static: false, read: TemplateRef }) template: TemplateRef; + @ContentChild(NzTabLinkDirective, { static: false }) linkDirective: NzTabLinkDirective; @Input() nzTitle: string | TemplateRef; + @Input() nzRouterIdentifier: string; @Input() @InputBoolean() nzForceRender = false; @Input() @InputBoolean() nzDisabled = false; @Output() readonly nzClick = new EventEmitter(); diff --git a/components/tabs/nz-tabs.module.ts b/components/tabs/nz-tabs.module.ts index cbd880327b1..3b62f7976ec 100644 --- a/components/tabs/nz-tabs.module.ts +++ b/components/tabs/nz-tabs.module.ts @@ -14,6 +14,7 @@ import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzTabBodyComponent } from './nz-tab-body.component'; import { NzTabLabelDirective } from './nz-tab-label.directive'; +import { NzTabLinkDirective } from './nz-tab-link.directive'; import { NzTabComponent } from './nz-tab.component'; import { NzTabDirective } from './nz-tab.directive'; import { NzTabsInkBarDirective } from './nz-tabs-ink-bar.directive'; @@ -28,7 +29,8 @@ import { NzTabSetComponent } from './nz-tabset.component'; NzTabsNavComponent, NzTabLabelDirective, NzTabsInkBarDirective, - NzTabBodyComponent + NzTabBodyComponent, + NzTabLinkDirective ], exports: [ NzTabComponent, @@ -37,7 +39,8 @@ import { NzTabSetComponent } from './nz-tabset.component'; NzTabsNavComponent, NzTabLabelDirective, NzTabsInkBarDirective, - NzTabBodyComponent + NzTabBodyComponent, + NzTabLinkDirective ], imports: [CommonModule, ObserversModule, NzIconModule, NzAddOnModule] }) diff --git a/components/tabs/nz-tabs.spec.ts b/components/tabs/nz-tabs.spec.ts index e99886abae9..bcae665f39a 100644 --- a/components/tabs/nz-tabs.spec.ts +++ b/components/tabs/nz-tabs.spec.ts @@ -1,6 +1,10 @@ +import { CommonModule } from '@angular/common'; import { Component, DebugElement, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; -import { fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing'; +import { fakeAsync, flush, tick, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { Router, Routes } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + import { NgStyleInterface } from 'ng-zorro-antd/core'; import { NzTabsModule } from './nz-tabs.module'; @@ -481,6 +485,67 @@ describe('tabs', () => { }); }); +describe('link router', () => { + let fixture: ComponentFixture; + let tabs: DebugElement; + let router: Router; + + describe('basic', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, NzTabsModule, RouterTestingModule.withRoutes(routes)], + declarations: [NzTestTabsLinkRouterComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(NzTestTabsLinkRouterComponent); + fixture.detectChanges(); + + tabs = fixture.debugElement.query(By.directive(NzTabSetComponent)); + }); + + it('should child route mode works', fakeAsync(() => { + fixture.ngZone!.run(() => { + router = TestBed.get(Router); + router.initialNavigation(); + + fixture.detectChanges(); + expect((tabs.componentInstance as NzTabSetComponent).nzSelectedIndex).toBe(0); + + router.navigate(['.', 'two']); + fixture.detectChanges(); + tick(200); + fixture.detectChanges(); + expect((tabs.componentInstance as NzTabSetComponent).nzSelectedIndex).toBe(1); + + flush(); + // const titles = tabs.nativeElement.querySelectorAll('.ant-tabs-tab'); + // titles[0].click(); + // fixture.detectChanges(); + // tick(200); + // fixture.detectChanges(); + // expect(location.path()).toBe('/'); + }); + })); + }); + + describe('throw error', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NzTabsModule], + declarations: [NzTestTabsLinkRouterComponent] + }); + }); + + it('should raise error when routerModule is not imported', () => { + expect(() => { + TestBed.compileComponents(); + fixture = TestBed.createComponent(NzTestTabsLinkRouterComponent); + fixture.detectChanges(); + }).toThrowError(); + }); + }); +}); + @Component({ encapsulation: ViewEncapsulation.None, styleUrls: ['../style/index.less', './style/index.less'], @@ -575,3 +640,37 @@ export class NzTestTabsBasicComponent { export class NzTestTabsTabPositionLeftComponent { tabs = [1, 2, 3]; } + +@Component({ + template: ` + + + One + One + + + Two + Two + + + + ` +}) +export class NzTestTabsLinkRouterComponent {} + +const routes: Routes = [ + { + path: '', + component: NzTestTabsLinkRouterComponent, + data: { + path: '' + } + }, + { + path: 'two', + component: NzTestTabsLinkRouterComponent, + data: { + path: 'two' + } + } +]; diff --git a/components/tabs/nz-tabset.component.html b/components/tabs/nz-tabset.component.html index 6cae947b799..4fddf9ba15d 100644 --- a/components/tabs/nz-tabset.component.html +++ b/components/tabs/nz-tabset.component.html @@ -28,7 +28,7 @@ [disabled]="tab.nzDisabled" (click)="clickLabel(i,tab.nzDisabled)" *ngFor="let tab of listOfNzTabComponent; let i = index"> - {{ tab.nzTitle }} + {{ tab.nzTitle }}
(); + tabPositionMode: NzTabPositionMode = 'horizontal'; @ContentChildren(NzTabComponent) listOfNzTabComponent: QueryList; @ViewChild(NzTabsNavComponent, { static: false }) nzTabsNavComponent: NzTabsNavComponent; @@ -89,6 +95,10 @@ export class NzTabSetComponent @Input() nzTabBarGutter: number; @Input() nzTabBarStyle: { [key: string]: string }; @Input() nzType: NzTabType = 'line'; + + @Input() @InputBoolean() nzLinkRouter = false; + @Input() nzQueryParam: string; + @Output() readonly nzOnNextClick = new EventEmitter(); @Output() readonly nzOnPrevClick = new EventEmitter(); @Output() readonly nzSelectChange: EventEmitter = new EventEmitter(true); @@ -144,8 +154,9 @@ export class NzTabSetComponent clickLabel(index: number, disabled: boolean): void { if (!disabled) { + const tabs = this.listOfNzTabComponent.toArray(); this.nzSelectedIndex = index; - this.listOfNzTabComponent.toArray()[index].nzClick.emit(); + tabs[index].nzClick.emit(); } } @@ -185,7 +196,8 @@ export class NzTabSetComponent private renderer: Renderer2, private nzUpdateHostClassService: NzUpdateHostClassService, private elementRef: ElementRef, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + @Optional() private router: Router ) {} ngOnChanges(changes: SimpleChanges): void { @@ -289,5 +301,45 @@ export class NzTabSetComponent ngAfterViewInit(): void { this.setPosition(this.nzTabPosition); + + if (this.nzLinkRouter) { + if (!this.router) { + throw new Error(`${PREFIX} you should import 'RouterModule' if you want to use 'nzLinkRouter'!`); + } + + this.router.events + .pipe( + takeUntil(this.destroy$), + filter(e => e instanceof NavigationEnd), + startWith(true) + ) + .subscribe(() => { + this.updateRouterActive(); + }); + } + } + + private updateRouterActive(): void { + if (this.router.navigated) { + const index = this.findShouldActiveTabIndex(); + if (index !== this._selectedIndex && index !== -1) { + this.nzSelectedIndex = index; + this.nzSelectedIndexChange.next(index); + } + } + } + + private findShouldActiveTabIndex(): number { + const tabs = this.listOfNzTabComponent.toArray(); + const isActive = this.isLinkActive(this.router); + + return tabs.findIndex(tab => { + const c = tab.linkDirective; + return c ? isActive(c.routerLink) || isActive(c.routerLinkWithHref) : false; + }); + } + + private isLinkActive(router: Router): (link?: RouterLink | RouterLinkWithHref) => boolean { + return (link?: RouterLink | RouterLinkWithHref) => (link ? router.isActive(link.urlTree, true) : false); } } diff --git a/components/tabs/public-api.ts b/components/tabs/public-api.ts index 5e54caaf057..d3a54c0ba16 100644 --- a/components/tabs/public-api.ts +++ b/components/tabs/public-api.ts @@ -14,3 +14,4 @@ export * from './nz-tabs.module'; export * from './nz-tabs-nav.component'; export * from './nz-tabset.component'; export * from './nz-tab.directive'; +export * from './nz-tab-link.directive'; diff --git a/components/tabs/style/entry.less b/components/tabs/style/entry.less index 06547c43acd..96cebe33bff 100644 --- a/components/tabs/style/entry.less +++ b/components/tabs/style/entry.less @@ -1 +1,2 @@ @import './index.less'; +@import './patch.less'; diff --git a/components/tabs/style/patch.less b/components/tabs/style/patch.less new file mode 100644 index 00000000000..6adb9753538 --- /dev/null +++ b/components/tabs/style/patch.less @@ -0,0 +1,12 @@ +a[nz-tab-link] { + &::before { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: transparent; + content: ''; + } +} + diff --git a/scripts/site/_site/doc/app/app.component.ts b/scripts/site/_site/doc/app/app.component.ts index 6bb3c0d4f9f..29d7c4912b5 100644 --- a/scripts/site/_site/doc/app/app.component.ts +++ b/scripts/site/_site/doc/app/app.component.ts @@ -107,7 +107,11 @@ export class AppComponent implements OnInit, AfterViewInit { this.searchComponent = null; } - this.language = this.router.url.split('/')[ this.router.url.split('/').length - 1 ].split('#')[ 0 ]; + this.language = this.router.url + .split('/')[this.router.url.split('/').length - 1] + .split('#')[0] + .split('?')[0]; + this.appService.language$.next(this.language); this.nzI18nService.setLocale(this.language === 'en' ? en_US : zh_CN); diff --git a/scripts/site/_site/doc/app/app.module.ts b/scripts/site/_site/doc/app/app.module.ts index 85570661406..258df85a572 100644 --- a/scripts/site/_site/doc/app/app.module.ts +++ b/scripts/site/_site/doc/app/app.module.ts @@ -45,7 +45,7 @@ const icons: IconDefinition[] = [LeftOutline, RightOutline]; NzIconModule, ShareModule, HttpClientJsonpModule, - RouterModule.forRoot(routes, environment.production ? { preloadingStrategy: PreloadAllModules } : {}), + RouterModule.forRoot(routes, environment.production ? { preloadingStrategy: PreloadAllModules, scrollPositionRestoration: 'enabled' } : {}), ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) ], providers: [