Skip to content

Commit a876cf9

Browse files
vaibhavbhalla2505Vaibhav  Bhalla
authored andcommitted
feat(arc): implement breadcrumb feature
GH-126
1 parent 62b36aa commit a876cf9

28 files changed

+704
-6
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const USERS = [
2+
{id: '123', name: 'John Doe', email: 'john.doe123@example.com'},
3+
{id: '124', name: 'Jane Smith', email: 'jane.smith124@example.com'},
4+
];
5+
export const TITLES = [
6+
{ id: '1', title: 'Contract.pdf' },
7+
{ id: '2', title: 'Appointment.pdf' },
8+
];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface User {
2+
id: string;
3+
name: string;
4+
email: string;
5+
}
6+
export interface Title {
7+
id: string;
8+
title: string;
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div class="container mt-4">
2+
<h2>Documentation</h2>
3+
<p><strong>ID:</strong>{{ title?.id }}</p>
4+
<p><strong>Title:</strong>{{ title?.title }}</p>
5+
</div>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component} from '@angular/core';
2+
import {ActivatedRoute} from '@angular/router';
3+
import { Title } from '../user-title.interface';
4+
5+
@Component({
6+
selector: 'lib-user-title',
7+
templateUrl: './user-title.component.html',
8+
})
9+
export class UserTitleComponent {
10+
title: Title;
11+
constructor(private readonly route: ActivatedRoute) {
12+
this.title = this.route.snapshot.data['document'];
13+
}
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {Injectable} from '@angular/core';
2+
import {ActivatedRouteSnapshot} from '@angular/router';
3+
import {Observable} from 'rxjs';
4+
import {TitleService} from './user-title.service';
5+
import { Title } from '../user-title.interface';
6+
7+
@Injectable()
8+
export class TitleResolver {
9+
constructor(private readonly titleService: TitleService) {}
10+
11+
resolve(route: ActivatedRouteSnapshot): Observable<Title> {
12+
const id = route.paramMap.get('id');
13+
return this.titleService.getTitleById(id);
14+
}
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Injectable} from '@angular/core';
2+
import {Observable, of} from 'rxjs';
3+
import { TITLES } from '../mock-data.constants';
4+
5+
@Injectable()
6+
export class TitleService {
7+
private readonly titles = TITLES
8+
9+
getTitleById(id: string): Observable<any> {
10+
const title = this.titles.find(u => u.id === id);
11+
return of(title);
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="container mt-4">
2+
<h2>User Details</h2>
3+
<p><strong>ID:</strong> {{ user?.id }}</p>
4+
<p><strong>Name:</strong> {{ user?.name }}</p>
5+
<p><strong>Email:</strong> {{ user?.email }}</p>
6+
7+
<router-outlet></router-outlet>
8+
</div>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {CommonModule} from '@angular/common';
2+
import {Component} from '@angular/core';
3+
import {ActivatedRoute, RouterModule} from '@angular/router';
4+
import { User } from '../user-title.interface';
5+
import { UserResolver } from './user.resolver';
6+
7+
@Component({
8+
selector: 'lib-user',
9+
standalone: true,
10+
templateUrl: './user.component.html',
11+
imports: [CommonModule, RouterModule],
12+
})
13+
export class UserComponent {
14+
user: User;
15+
16+
constructor(private readonly route: ActivatedRoute) {
17+
this.user = this.route.snapshot.data['user'];
18+
}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {Injectable} from '@angular/core';
2+
import {ActivatedRouteSnapshot} from '@angular/router';
3+
import {Observable} from 'rxjs';
4+
import {UserService} from './user.service';
5+
import {User} from '../user-title.interface';
6+
7+
@Injectable()
8+
export class UserResolver {
9+
constructor(private readonly userService: UserService) {}
10+
11+
resolve(route: ActivatedRouteSnapshot): Observable<User> {
12+
const id = route.paramMap.get('id');
13+
return this.userService.getUserById(id);
14+
}
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Injectable} from '@angular/core';
2+
import {Observable, of} from 'rxjs';
3+
import { USERS } from '../mock-data.constants';
4+
5+
@Injectable()
6+
export class UserService {
7+
private readonly users = USERS
8+
9+
getUserById(id: string): Observable<any> {
10+
const user = this.users.find(u => u.id === id);
11+
return of(user);
12+
}
13+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<nav aria-label="breadcrumb" *ngIf="breadcrumbs$ | async as breadcrumbs">
2+
<ul class="breadcrumb">
3+
<ng-container
4+
*ngIf="breadcrumbs.length > maxItems && !expanded; else fullBreadcrumb"
5+
>
6+
<li class="breadcrumb-item">
7+
<ng-container *ngIf="!breadcrumbs[0].skipLink; else noLinkFirst">
8+
<a [routerLink]="breadcrumbs[0].url">{{ breadcrumbs[0].label }}</a>
9+
</ng-container>
10+
<ng-template #noLinkFirst>
11+
<span>{{ breadcrumbs[0].label }}</span>
12+
</ng-template>
13+
</li>
14+
<span class="separator">{{separator}}</span>
15+
16+
<li class="breadcrumb-item clickable" (click)="toggleExpand()">...</li>
17+
<span class="separator">{{separator}}</span>
18+
19+
<li class="breadcrumb-item" [class.active]="true">
20+
<ng-container
21+
*ngIf="!breadcrumbs[breadcrumbs.length - 1].skipLink; else noLinkLast"
22+
>
23+
<a [routerLink]="breadcrumbs[breadcrumbs.length - 1].url">
24+
{{ breadcrumbs[breadcrumbs.length - 1].label }}
25+
</a>
26+
</ng-container>
27+
<ng-template #noLinkLast>
28+
<span>{{ breadcrumbs[breadcrumbs.length - 1].label }}</span>
29+
</ng-template>
30+
</li>
31+
</ng-container>
32+
33+
<ng-template #fullBreadcrumb>
34+
<ng-container *ngFor="let breadcrumb of breadcrumbs; let last = last">
35+
<li class="breadcrumb-item" [class.active]="last">
36+
<ng-container *ngIf="!breadcrumb.skipLink && !last; else noLink">
37+
<a [routerLink]="breadcrumb.url">{{ breadcrumb.label }}</a>
38+
</ng-container>
39+
<ng-template #noLink>
40+
<span>{{ breadcrumb.label }}</span>
41+
</ng-template>
42+
</li>
43+
<span *ngIf="!last" class="separator">{{separator}}</span>
44+
</ng-container>
45+
</ng-template>
46+
</ul>
47+
</nav>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.breadcrumb {
2+
display: flex;
3+
flex-wrap: wrap;
4+
list-style: none;
5+
padding: 0;
6+
margin: 0;
7+
background-color: transparent;
8+
9+
10+
.breadcrumb-item {
11+
display: flex;
12+
align-items: center;
13+
font-size: 14px;
14+
color: #555;
15+
16+
a {
17+
color: #007bff;
18+
text-decoration: none;
19+
20+
&:hover {
21+
text-decoration: underline;
22+
}
23+
}
24+
25+
&.active {
26+
font-weight: 600;
27+
color: #333;
28+
}
29+
&.clickable {
30+
cursor: pointer;
31+
color: #007bff;
32+
text-decoration: underline;
33+
34+
&:hover {
35+
color: #0056b3;
36+
}
37+
}
38+
}
39+
40+
.separator {
41+
margin: 0 6px;
42+
color: #aaa;
43+
font-size: 14px;
44+
}
45+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {Component, Input, isDevMode, OnInit} from '@angular/core';
2+
import {CommonModule} from '@angular/common';
3+
import {Breadcrumb} from './breadcrumb.interface';
4+
import {BreadcrumbService} from './breadcrumb.service';
5+
import {Observable} from 'rxjs';
6+
import {RouterModule} from '@angular/router';
7+
8+
@Component({
9+
selector: 'app-breadcrumb',
10+
templateUrl: './breadcrumb.component.html',
11+
standalone: true,
12+
imports: [CommonModule, RouterModule],
13+
styleUrls: ['./breadcrumb.component.scss'],
14+
})
15+
export class BreadcrumbComponent implements OnInit {
16+
breadcrumbs$: Observable<Breadcrumb[]> = this.breadcrumbService.breadcrumbs;
17+
18+
@Input() staticBreadcrumbs = [];
19+
@Input() separator = '>';
20+
@Input() maxItems = 8;
21+
@Input() separatorClass = 'separator';
22+
@Input() itemClass = 'breadcrumb-item';
23+
24+
expanded = false;
25+
constructor(private readonly breadcrumbService: BreadcrumbService) {}
26+
ngOnInit(): void {
27+
this.breadcrumbs$.subscribe(breadcrumbs => {
28+
if (isDevMode()) {
29+
console.log('Breadcrumbs:', breadcrumbs);
30+
}
31+
});
32+
}
33+
toggleExpand() {
34+
this.expanded = true;
35+
}
36+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface Breadcrumb {
2+
label: string;
3+
url: string;
4+
skipLink?: boolean;
5+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {Injectable} from '@angular/core';
2+
import {ActivatedRouteSnapshot, NavigationEnd, Router} from '@angular/router';
3+
import {BehaviorSubject} from 'rxjs';
4+
import {filter} from 'rxjs/operators';
5+
import {Breadcrumb} from './breadcrumb.interface';
6+
7+
@Injectable({providedIn: 'root'})
8+
export class BreadcrumbService {
9+
private readonly breadcrumbs$ = new BehaviorSubject<Breadcrumb[]>([]);
10+
11+
constructor(private readonly router: Router) {
12+
this.router.events
13+
.pipe(filter(event => event instanceof NavigationEnd))
14+
.subscribe(() => {
15+
const root = this.router.routerState.snapshot.root;
16+
const breadcrumbs = this.buildBreadcrumbs(root);
17+
this.breadcrumbs$.next(breadcrumbs);
18+
});
19+
}
20+
21+
private buildBreadcrumbs(
22+
route: ActivatedRouteSnapshot,
23+
url = '',
24+
breadcrumbs: Breadcrumb[] = [],
25+
): Breadcrumb[] {
26+
if (!route.routeConfig) {
27+
return route.firstChild
28+
? this.buildBreadcrumbs(route.firstChild, url, breadcrumbs)
29+
: breadcrumbs;
30+
}
31+
32+
let path = route.routeConfig.path || '';
33+
Object.keys(route.params).forEach(key => {
34+
path = path.replace(`:${key}`, route.params[key]);
35+
});
36+
const nextUrl = path ? `${url}/${path}` : url;
37+
const label = this._resolveLabel(route, path);
38+
const skipLink = route.routeConfig.data?.['skipLink'] ?? false;
39+
40+
if (label) {
41+
breadcrumbs.push({label, url: nextUrl, skipLink});
42+
}
43+
44+
return route.firstChild
45+
? this.buildBreadcrumbs(route.firstChild, nextUrl, breadcrumbs)
46+
: breadcrumbs;
47+
}
48+
49+
private _toTitleCase(str: string): string {
50+
return str.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
51+
}
52+
53+
private _resolveLabel(route: ActivatedRouteSnapshot, path: string): string {
54+
const breadcrumbData = route.routeConfig.data?.['breadcrumb'];
55+
56+
if (typeof breadcrumbData === 'function') {
57+
return breadcrumbData(route.data, route.paramMap, route);
58+
}
59+
if (typeof breadcrumbData === 'string') {
60+
return breadcrumbData;
61+
}
62+
if (route.routeConfig.path?.startsWith(':')) {
63+
const paramName = route.routeConfig.path.slice(1);
64+
return route.params[paramName] ?? paramName;
65+
}
66+
return this._toTitleCase(path);
67+
}
68+
69+
get breadcrumbs() {
70+
return this.breadcrumbs$.asObservable();
71+
}
72+
}

projects/arc-lib/src/lib/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './gantt/gantt.module';
44
export * from './selector/select.module';
55
export * from './resize/resize.module';
66
export * from './list/list.component';
7+
export * from './breadcrumb/breadcrumb.component';

projects/arc/src/app/app-routing.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const routes: Routes = [
1515
{
1616
path: 'main',
1717
loadChildren: () => import('./main/main.module').then(m => m.MainModule),
18+
data: {skipLink: true},
1819
canActivate: [AuthGuard],
1920
},
2021
{

projects/arc/src/app/app.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {environment} from '../environments/environment';
1919
import {ThemeModule} from '@project-lib/theme/theme.module';
2020
import {OverlayModule} from '@angular/cdk/overlay';
2121
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
22-
import {GanttModule} from '@project-lib/components/index';
22+
import {BreadcrumbComponent, GanttModule} from '@project-lib/components/index';
2323
import {SelectModule} from '@project-lib/components/selector';
2424
import {HeaderComponent} from '@project-lib/components/header/header.component';
2525
import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.component';
@@ -40,6 +40,7 @@ import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.componen
4040
BrowserAnimationsModule,
4141
HeaderComponent,
4242
SidebarComponent,
43+
BreadcrumbComponent,
4344
],
4445
providers: [
4546
TranslationService,

0 commit comments

Comments
 (0)