Skip to content

Commit

Permalink
feat(module:breadcrumb): support auto generated breadcrumb (#2050)
Browse files Browse the repository at this point in the history
close #2001
  • Loading branch information
Wendell authored and Jason committed Sep 7, 2018
1 parent 2858ba1 commit 64d191c
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 5 deletions.
18 changes: 18 additions & 0 deletions components/breadcrumb/demo/auto.md
Original file line number Diff line number Diff line change
@@ -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`.
11 changes: 11 additions & 0 deletions components/breadcrumb/demo/auto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';

@Component({
selector: 'nz-demo-breadcrumb-auto',
template: `
<nz-breadcrumb [nzAutoGenerate]="true">
Please refer to StackBlitz demo.
</nz-breadcrumb>
`
})
export class NzDemoBreadcrumbAutoComponent {}
12 changes: 12 additions & 0 deletions components/breadcrumb/doc/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>` | | `/` |
| `[nzAutoGenerate]` | Auto generate breadcrumb | boolean | | `false` |

Using `[nzAutoGenerate]` by configuring `data` like this:

```ts
{
path: '/path',
component: SomeComponent,
data: {
breadcrumb: 'Display Name'
}
}
```
13 changes: 13 additions & 0 deletions components/breadcrumb/doc/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ title: Breadcrumb
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| --- | --- | --- | --- | --- |
| `[nzSeparator]` | 分隔符自定义 | string丨`TemplateRef<void>` | | '/' |
| `[nzAutoGenerate]` | 自动生成 Breadcrumb | boolean | | `false` |

使用 `[nzAutoGenerate]` 时,需要在路由类中定义 `data`:

```ts
{
path: '/path',
component: SomeComponent,
data: {
breadcrumb: 'Display Name'
}
}
```
7 changes: 6 additions & 1 deletion components/breadcrumb/nz-breadcrumb.component.html
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<ng-content></ng-content>
<ng-content></ng-content>
<ng-container *ngIf="nzAutoGenerate">
<nz-breadcrumb-item *ngFor="let breadcrumb of breadcrumbs">
<a [attr.href]="breadcrumb.url">{{ breadcrumb.label }}</a>
</nz-breadcrumb-item>
</ng-container>
68 changes: 67 additions & 1 deletion components/breadcrumb/nz-breadcrumb.component.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -17,10 +31,13 @@ import {
}
` ]
})
export class NzBreadCrumbComponent {
export class NzBreadCrumbComponent implements OnInit, OnDestroy {
private _separator: string | TemplateRef<void> = '/';
private $destroy = new Subject();
isTemplateRef = false;

@Input() nzAutoGenerate = false;

@Input()
set nzSeparator(value: string | TemplateRef<void>) {
this._separator = value;
Expand All @@ -30,4 +47,53 @@ export class NzBreadCrumbComponent {
get nzSeparator(): string | TemplateRef<void> {
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();
}
}
3 changes: 1 addition & 2 deletions components/breadcrumb/nz-breadcrumb.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ import { NzBreadCrumbComponent } from './nz-breadcrumb.component';
declarations: [ NzBreadCrumbComponent, NzBreadCrumbItemComponent ],
exports : [ NzBreadCrumbComponent, NzBreadCrumbItemComponent ]
})
export class NzBreadCrumbModule {
}
export class NzBreadCrumbModule {}
144 changes: 143 additions & 1 deletion components/breadcrumb/nz-breadcrumb.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +14,7 @@ import { NzBreadCrumbModule } from './nz-breadcrumb.module';
describe('breadcrumb', () => {
let testComponent;
let fixture;

describe('basic', () => {
let items;
let breadcrumb;
Expand All @@ -35,6 +40,7 @@ describe('breadcrumb', () => {
expect(breadcrumb.nativeElement.classList.contains('ant-breadcrumb')).toBe(true);
});
});

describe('separator', () => {
let items;
let breadcrumbs;
Expand Down Expand Up @@ -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: `
<nz-breadcrumb [nzAutoGenerate]="true"></nz-breadcrumb>
<router-outlet></router-outlet>
<router-outlet name="notprimary"></router-outlet>
`
})
export class NzBreadcrumbAutoGenerateDemoComponent {
}

@Component({
selector: 'nz-breadcrumb-auto-generate-error-demo',
template: '<nz-breadcrumb [nzAutoGenerate]="autoGenerate"></nz-breadcrumb>'
})
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
}
]
}
];

0 comments on commit 64d191c

Please sign in to comment.