From 64d191cba4e28f76b60e845f7d6db603401e3cc0 Mon Sep 17 00:00:00 2001 From: Wendell Date: Fri, 7 Sep 2018 19:25:30 +0800 Subject: [PATCH] feat(module:breadcrumb): support auto generated breadcrumb (#2050) close #2001 --- components/breadcrumb/demo/auto.md | 18 +++ components/breadcrumb/demo/auto.ts | 11 ++ components/breadcrumb/doc/index.en-US.md | 12 ++ components/breadcrumb/doc/index.zh-CN.md | 13 ++ .../breadcrumb/nz-breadcrumb.component.html | 7 +- .../breadcrumb/nz-breadcrumb.component.ts | 68 ++++++++- components/breadcrumb/nz-breadcrumb.module.ts | 3 +- components/breadcrumb/nz-breadcrumb.spec.ts | 144 +++++++++++++++++- 8 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 components/breadcrumb/demo/auto.md create mode 100644 components/breadcrumb/demo/auto.ts diff --git a/components/breadcrumb/demo/auto.md b/components/breadcrumb/demo/auto.md new file mode 100644 index 0000000000..2bf7a024d0 --- /dev/null +++ b/components/breadcrumb/demo/auto.md @@ -0,0 +1,18 @@ +--- +order: 4 + +iframe: + source: https://stackblitz.com/edit/ng-zorro-breadcrumb-auto?embed=1&file=src/app/app.component.html&hideExplorer=1&hideNavigation=1&view=preview + height: 460 +title: + zh-CN: 自动生成 + en-US: Auto generated breadcrumbs +--- + +## zh-CN + +通过配置 `router.data` 自动生成面包屑。 + +## en-US + +Auto generate breadcrumbs using `router.data`. diff --git a/components/breadcrumb/demo/auto.ts b/components/breadcrumb/demo/auto.ts new file mode 100644 index 0000000000..2c9bcf996d --- /dev/null +++ b/components/breadcrumb/demo/auto.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-breadcrumb-auto', + template: ` + + Please refer to StackBlitz demo. + + ` +}) +export class NzDemoBreadcrumbAutoComponent {} diff --git a/components/breadcrumb/doc/index.en-US.md b/components/breadcrumb/doc/index.en-US.md index 7b3603ed6d..56199e7c49 100755 --- a/components/breadcrumb/doc/index.en-US.md +++ b/components/breadcrumb/doc/index.en-US.md @@ -20,4 +20,16 @@ A breadcrumb displays the current location within a hierarchy. It allows going b | Property | Description | Type | Optional | Default | | -------- | ----------- | ---- | -------- | ------- | | `[nzSeparator]` | Custom separator | string丨`TemplateRef` | | `/` | +| `[nzAutoGenerate]` | Auto generate breadcrumb | boolean | | `false` | +Using `[nzAutoGenerate]` by configuring `data` like this: + +```ts +{ + path: '/path', + component: SomeComponent, + data: { + breadcrumb: 'Display Name' + } +} +``` diff --git a/components/breadcrumb/doc/index.zh-CN.md b/components/breadcrumb/doc/index.zh-CN.md index bab7de8a0d..8a61ba30da 100755 --- a/components/breadcrumb/doc/index.zh-CN.md +++ b/components/breadcrumb/doc/index.zh-CN.md @@ -20,3 +20,16 @@ title: Breadcrumb | 参数 | 说明 | 类型 | 可选值 | 默认值 | | --- | --- | --- | --- | --- | | `[nzSeparator]` | 分隔符自定义 | string丨`TemplateRef` | | '/' | +| `[nzAutoGenerate]` | 自动生成 Breadcrumb | boolean | | `false` | + +使用 `[nzAutoGenerate]` 时,需要在路由类中定义 `data`: + +```ts +{ + path: '/path', + component: SomeComponent, + data: { + breadcrumb: 'Display Name' + } +} +``` diff --git a/components/breadcrumb/nz-breadcrumb.component.html b/components/breadcrumb/nz-breadcrumb.component.html index 95a0b70bdc..781fcbd4bc 100755 --- a/components/breadcrumb/nz-breadcrumb.component.html +++ b/components/breadcrumb/nz-breadcrumb.component.html @@ -1 +1,6 @@ - \ No newline at end of file + + + + {{ breadcrumb.label }} + + diff --git a/components/breadcrumb/nz-breadcrumb.component.ts b/components/breadcrumb/nz-breadcrumb.component.ts index 2d83403281..a8d741d487 100755 --- a/components/breadcrumb/nz-breadcrumb.component.ts +++ b/components/breadcrumb/nz-breadcrumb.component.ts @@ -1,8 +1,22 @@ import { Component, + Injector, Input, + OnDestroy, + OnInit, TemplateRef } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Params, PRIMARY_OUTLET, Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import { filter, takeUntil } from 'rxjs/operators'; + +const ROUTE_DATA_BREADCRUMB = 'breadcrumb'; + +export interface BreadcrumbOption { + label: string; + params: Params; + url: string; +} @Component({ selector : 'nz-breadcrumb', @@ -17,10 +31,13 @@ import { } ` ] }) -export class NzBreadCrumbComponent { +export class NzBreadCrumbComponent implements OnInit, OnDestroy { private _separator: string | TemplateRef = '/'; + private $destroy = new Subject(); isTemplateRef = false; + @Input() nzAutoGenerate = false; + @Input() set nzSeparator(value: string | TemplateRef) { this._separator = value; @@ -30,4 +47,53 @@ export class NzBreadCrumbComponent { get nzSeparator(): string | TemplateRef { return this._separator; } + + breadcrumbs: BreadcrumbOption[] = []; + + getBreadcrumbs(route: ActivatedRoute, url: string = '', breadcrumbs: BreadcrumbOption[] = []): BreadcrumbOption[] { + const children: ActivatedRoute[] = route.children; + if (children.length === 0) { + return breadcrumbs; // If there's no sub root, then stop the recurse and returns the generated breadcrumbs. + } + for (const child of children) { + if (child.outlet !== PRIMARY_OUTLET) { + continue; // Only parse components in primary router-outlet (in another word, router-outlet without a specific name). + } else { + // Parse this layer and generate a breadcrumb item. + const routeURL: string = child.snapshot.url.map(segment => segment.path).join('/'); + const nextUrl = url + `/${routeURL}`; + // If have data, go to generate a breadcrumb for it. + if (child.snapshot.data.hasOwnProperty(ROUTE_DATA_BREADCRUMB)) { + const breadcrumb: BreadcrumbOption = { + label : child.snapshot.data[ ROUTE_DATA_BREADCRUMB ] || 'Breadcrumb', + params: child.snapshot.params, + url : nextUrl + }; + breadcrumbs.push(breadcrumb); + } + return this.getBreadcrumbs(child, nextUrl, breadcrumbs); + } + } + } + + constructor(private _injector: Injector) {} + + ngOnInit(): void { + if (this.nzAutoGenerate) { + try { + const activatedRoute = this._injector.get(ActivatedRoute); + const router = this._injector.get(Router); + router.events.pipe(filter(e => e instanceof NavigationEnd), takeUntil(this.$destroy)).subscribe(() => { + this.breadcrumbs = this.getBreadcrumbs(activatedRoute.root); // Build the breadcrumb tree from root route. + }); + } catch (e) { + throw new Error('You should import RouterModule if you want to use NzAutoGenerate'); + } + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } } diff --git a/components/breadcrumb/nz-breadcrumb.module.ts b/components/breadcrumb/nz-breadcrumb.module.ts index 93ba156578..3d7a4bc0fe 100644 --- a/components/breadcrumb/nz-breadcrumb.module.ts +++ b/components/breadcrumb/nz-breadcrumb.module.ts @@ -9,5 +9,4 @@ import { NzBreadCrumbComponent } from './nz-breadcrumb.component'; declarations: [ NzBreadCrumbComponent, NzBreadCrumbItemComponent ], exports : [ NzBreadCrumbComponent, NzBreadCrumbItemComponent ] }) -export class NzBreadCrumbModule { -} +export class NzBreadCrumbModule {} diff --git a/components/breadcrumb/nz-breadcrumb.spec.ts b/components/breadcrumb/nz-breadcrumb.spec.ts index 79f76c1459..2ee880b6da 100644 --- a/components/breadcrumb/nz-breadcrumb.spec.ts +++ b/components/breadcrumb/nz-breadcrumb.spec.ts @@ -1,6 +1,10 @@ -import { async, TestBed } from '@angular/core/testing'; +import { Component, NgZone } from '@angular/core'; +import { async, fakeAsync, flush, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { Router, Routes } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CommonModule } from '@angular/common'; import { NzDemoBreadcrumbBasicComponent } from './demo/basic'; import { NzDemoBreadcrumbSeparatorComponent } from './demo/separator'; import { NzBreadCrumbItemComponent } from './nz-breadcrumb-item.component'; @@ -10,6 +14,7 @@ import { NzBreadCrumbModule } from './nz-breadcrumb.module'; describe('breadcrumb', () => { let testComponent; let fixture; + describe('basic', () => { let items; let breadcrumb; @@ -35,6 +40,7 @@ describe('breadcrumb', () => { expect(breadcrumb.nativeElement.classList.contains('ant-breadcrumb')).toBe(true); }); }); + describe('separator', () => { let items; let breadcrumbs; @@ -62,4 +68,140 @@ describe('breadcrumb', () => { expect(items[ 3 ].nativeElement.children[ 1 ].firstElementChild.classList.contains('anticon-arrow-right')).toBe(true); }); }); + + describe('auto generated', () => { + let breadcrumb; + let router; + + it('should support auto generating', fakeAsync(() => { + // Prepare test bed. + TestBed.configureTestingModule({ + imports : [ CommonModule, NzBreadCrumbModule, RouterTestingModule.withRoutes(routes) ], + declarations: [ NzBreadcrumbAutoGenerateDemoComponent, NzBreadcrumbNullComponent ] + }).compileComponents(); + fixture = TestBed.createComponent(NzBreadcrumbAutoGenerateDemoComponent); + fixture.detectChanges(); + breadcrumb = fixture.debugElement.query(By.directive(NzBreadCrumbComponent)); + testComponent = fixture.debugElement.componentInstance; + router = TestBed.get(Router); + // A bug of Angular forces us to use zone now and cannot test a tag (it works, but Karma would timeout), see: https://github.com/angular/angular/issues/25837. + const zone = TestBed.get(NgZone); + router.initialNavigation(); + zone.run(() => { + router.navigate([ 'one', 'two', 'three', 'four' ]); + }); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(2); // Should generate 2 breadrumbs when reaching out of the `data` scope. + // items = breadcrumb.nativeElement.querySelectorAll('.ant-breadcrumb-link a'); + // dispatchMouseEvent(items[ 1 ], 'click'); // A link should work. + zone.run(() => { + router.navigate([ 'one', 'two', 'three' ]); + }); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(2); + // dispatchMouseEvent(items[ 0 ], 'click'); // A link should work. + zone.run(() => { + router.navigate([ 'one', 'two' ]); + }); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(1); + zone.run(() => { + router.navigate([ 'one' ]); + }); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(breadcrumb.componentInstance.breadcrumbs.length).toBe(0); // Shouldn't generate breadcrumb at all. + })); + + it('should raise error when RouterModule is not included', fakeAsync(() => { + TestBed.configureTestingModule({ + imports : [ NzBreadCrumbModule ], // no RouterTestingModule + declarations: [ NzBreadcrumbAutoGenerateErrorDemoComponent ] + }); + expect(() => { + TestBed.compileComponents(); + fixture = TestBed.createComponent(NzBreadcrumbAutoGenerateErrorDemoComponent); + testComponent = fixture.debugElement.componentInstance; + fixture.detectChanges(); + }).toThrowError(); + })); + + it('should not raise error when autoGenerate is not used', fakeAsync(() => { + TestBed.configureTestingModule({ + imports : [ NzBreadCrumbModule ], + declarations: [ NzBreadcrumbAutoGenerateErrorDemoComponent ] + }).compileComponents(); + fixture = TestBed.createComponent(NzBreadcrumbAutoGenerateErrorDemoComponent); + testComponent = fixture.debugElement.componentInstance; + testComponent.autoGenerate = false; + expect(() => { + fixture.detectChanges(); + }).not.toThrowError(); + })); + }); }); + +@Component({ + selector: 'nz-breadcrumb-auto-generate-demo', + template: ` + + + + ` +}) +export class NzBreadcrumbAutoGenerateDemoComponent { +} + +@Component({ + selector: 'nz-breadcrumb-auto-generate-error-demo', + template: '' +}) +export class NzBreadcrumbAutoGenerateErrorDemoComponent { + autoGenerate = true; +} + +@Component({ + selector: 'nz-breadcrumb-null', + template: '' +}) +export class NzBreadcrumbNullComponent { +} + +const routes: Routes = [ + { + path : 'one', + component: NzBreadcrumbAutoGenerateDemoComponent, + children : [ + { + path : 'two', + component: NzBreadcrumbNullComponent, + data : { breadcrumb: 'Layer 2' }, + children : [ + { + path : 'three', + component: NzBreadcrumbNullComponent, + data : { breadcrumb: '' }, + children : [ + { + path : 'four', + component: NzBreadcrumbNullComponent + } + ] + } + ] + }, + { + path : 'two', + outlet : 'notprimary', + component: NzBreadcrumbNullComponent + } + ] + } +];