Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(module:breadcrumb): support auto generated breadcrumbs #2050

Merged
merged 1 commit into from
Sep 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
]
}
];