Skip to content

Commit bd931a5

Browse files
Vaibhav  BhallaVaibhav  Bhalla
authored andcommitted
feat(arc): implement breadcrumb feature
GH-126
1 parent 3f9dda5 commit bd931a5

File tree

17 files changed

+264
-155
lines changed

17 files changed

+264
-155
lines changed

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
export const USERS = [
1+
import {TitleDetails, UserDetails} from './user-title.interface';
2+
3+
export const USERS: UserDetails[] = [
24
{id: '123', name: 'John Doe', email: 'john.doe123@example.com'},
35
{id: '124', name: 'Jane Smith', email: 'jane.smith124@example.com'},
46
];
5-
export const TITLES = [
7+
export const TITLES: TitleDetails[] = [
68
{id: '1', title: 'Contract.pdf'},
79
{id: '2', title: 'Appointment.pdf'},
810
];
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
export interface User {
1+
export interface UserDetails {
22
id: string;
33
name: string;
44
email: string;
55
}
6-
export interface Title {
6+
export interface TitleDetails {
77
id: string;
88
title: string;
99
}
Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import {Component} from '@angular/core';
22
import {ActivatedRoute} from '@angular/router';
3-
import {Title} from '../user-title.interface';
3+
import {TitleDetails} from '../user-title.interface';
4+
import {TitleService} from './user-title.service';
45

56
@Component({
67
selector: 'lib-user-title',
78
templateUrl: './user-title.component.html',
89
})
910
export class UserTitleComponent {
10-
title: Title;
11-
constructor(private readonly route: ActivatedRoute) {
12-
this.title = this.route.snapshot.data['document'];
11+
title: TitleDetails;
12+
constructor(
13+
private readonly route: ActivatedRoute,
14+
private readonly titleService: TitleService,
15+
) {
16+
const id = this.route.snapshot.paramMap.get('id');
17+
if (id) {
18+
this.titleService.getTitleById(id).subscribe(title => {
19+
this.title = title;
20+
});
21+
}
1322
}
1423
}

projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.resolver.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import {Injectable} from '@angular/core';
2-
import {Observable, of} from 'rxjs';
1+
import {catchError, delay, map, Observable, of} from 'rxjs';
32
import {TITLES} from '../mock-data.constants';
3+
import {TitleDetails} from '../user-title.interface';
44

5-
@Injectable()
65
export class TitleService {
76
private readonly titles = TITLES;
87

9-
getTitleById(id: string): Observable<any> {
8+
getTitleById(id: string): Observable<TitleDetails> {
109
const title = this.titles.find(u => u.id === id);
1110
return of(title);
1211
}
12+
getTitleNameForBreadcrumb(id: string): Observable<string> {
13+
return this.getTitleById(id).pipe(
14+
map(titles => titles?.title || `Document #${id}`),
15+
catchError(() => of(`Document #${id}`)),
16+
delay(2000), // Simulating network delay
17+
);
18+
}
1319
}
Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {CommonModule} from '@angular/common';
22
import {Component} from '@angular/core';
33
import {ActivatedRoute, RouterModule} from '@angular/router';
4-
import {User} from '../user-title.interface';
5-
import {UserResolver} from './user.resolver';
4+
import {UserDetails} from '../user-title.interface';
5+
import {UserService} from './user.service';
66

77
@Component({
88
selector: 'lib-user',
@@ -11,9 +11,17 @@ import {UserResolver} from './user.resolver';
1111
imports: [CommonModule, RouterModule],
1212
})
1313
export class UserComponent {
14-
user: User;
14+
user: UserDetails;
1515

16-
constructor(private readonly route: ActivatedRoute) {
17-
this.user = this.route.snapshot.data['user'];
16+
constructor(
17+
private readonly route: ActivatedRoute,
18+
private readonly userService: UserService,
19+
) {
20+
const id = this.route.snapshot.paramMap.get('id');
21+
if (id) {
22+
this.userService.getUserById(id).subscribe(user => {
23+
this.user = user;
24+
});
25+
}
1826
}
1927
}

projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.resolver.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import {Injectable} from '@angular/core';
2-
import {Observable, of} from 'rxjs';
1+
import {catchError, delay, map, Observable, of} from 'rxjs';
32
import {USERS} from '../mock-data.constants';
3+
import {UserDetails} from '../user-title.interface';
44

5-
@Injectable()
65
export class UserService {
76
private readonly users = USERS;
87

9-
getUserById(id: string): Observable<any> {
8+
getUserById(id: string): Observable<UserDetails> {
109
const user = this.users.find(u => u.id === id);
1110
return of(user);
1211
}
12+
getUserNameForBreadcrumb(id: string): Observable<string> {
13+
return this.getUserById(id).pipe(
14+
map(user => user?.name || `User #${id}`),
15+
catchError(() => of(`User #${id}`)),
16+
delay(4000),
17+
);
18+
}
1319
}

projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Component, Input, isDevMode, OnInit} from '@angular/core';
22
import {CommonModule} from '@angular/common';
33
import {Breadcrumb} from './breadcrumb.interface';
44
import {BreadcrumbService} from './breadcrumb.service';
5-
import {Observable} from 'rxjs';
5+
import {Observable, Subject, takeUntil} from 'rxjs';
66
import {RouterModule} from '@angular/router';
77

88
@Component({
@@ -22,14 +22,19 @@ export class BreadcrumbComponent implements OnInit {
2222
@Input() itemClass = 'breadcrumb-item';
2323

2424
expanded = false;
25+
private destroy$ = new Subject<void>();
2526
constructor(private readonly breadcrumbService: BreadcrumbService) {}
2627
ngOnInit(): void {
27-
this.breadcrumbs$.subscribe(breadcrumbs => {
28+
this.breadcrumbs$.pipe(takeUntil(this.destroy$)).subscribe(breadcrumbs => {
2829
if (isDevMode()) {
2930
console.log('Breadcrumbs:', breadcrumbs);
3031
}
3132
});
3233
}
34+
ngOnDestroy(): void {
35+
this.destroy$.next();
36+
this.destroy$.complete();
37+
}
3338
toggleExpand() {
3439
this.expanded = true;
3540
}

projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.service.ts

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Injectable} from '@angular/core';
1+
import {Injectable, Injector} from '@angular/core';
22
import {ActivatedRouteSnapshot, NavigationEnd, Router} from '@angular/router';
33
import {BehaviorSubject} from 'rxjs';
44
import {filter} from 'rxjs/operators';
@@ -8,10 +8,13 @@ import {Breadcrumb} from './breadcrumb.interface';
88
export class BreadcrumbService {
99
private readonly breadcrumbs$ = new BehaviorSubject<Breadcrumb[]>([]);
1010

11-
constructor(private readonly router: Router) {
11+
constructor(
12+
private readonly router: Router,
13+
private readonly injector: Injector,
14+
) {
1215
this.router.events
1316
.pipe(filter(event => event instanceof NavigationEnd))
14-
.subscribe(() => {
17+
.subscribe(async () => {
1518
const root = this.router.routerState.snapshot.root;
1619
const breadcrumbs = this.buildBreadcrumbs(root);
1720
this.breadcrumbs$.next(breadcrumbs);
@@ -30,14 +33,16 @@ export class BreadcrumbService {
3033
}
3134

3235
let path = route.routeConfig.path || '';
36+
37+
// Replace route params like :id with actual values
3338
Object.keys(route.params).forEach(key => {
3439
path = path.replace(`:${key}`, route.params[key]);
3540
});
3641
const nextUrl = path ? `${url}/${path}` : url;
37-
const label = this._resolveLabel(route, path);
42+
const label = this._resolveLabel(route, path, nextUrl);
3843
const skipLink = route.routeConfig.data?.['skipLink'] ?? false;
3944

40-
if (label) {
45+
if (label && label.trim() !== '') {
4146
breadcrumbs.push({label, url: nextUrl, skipLink});
4247
}
4348

@@ -50,22 +55,62 @@ export class BreadcrumbService {
5055
return str.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
5156
}
5257

53-
private _resolveLabel(route: ActivatedRouteSnapshot, path: string): string {
54-
const breadcrumbData = route.routeConfig.data?.['breadcrumb'];
58+
private _resolveLabel(
59+
route: ActivatedRouteSnapshot,
60+
path: string,
61+
currentUrl: string,
62+
): string {
63+
const data = route.routeConfig?.data;
5564

56-
if (typeof breadcrumbData === 'function') {
57-
return breadcrumbData(route.data, route.paramMap, route);
58-
}
59-
if (typeof breadcrumbData === 'string') {
60-
return breadcrumbData;
65+
//async breadcrumb logic
66+
const asyncConfig = data?.asyncBreadcrumb;
67+
if (asyncConfig?.service && asyncConfig?.method) {
68+
const params = route.paramMap;
69+
const paramValue = params.get('id');
70+
const fallback =
71+
asyncConfig.fallbackLabel?.(params) || this._toTitleCase(path);
72+
const loadingLabel = asyncConfig.loadingLabel || fallback;
73+
74+
setTimeout(async () => {
75+
try {
76+
const serviceInstance = this.injector.get(asyncConfig.service);
77+
const result$ = serviceInstance[asyncConfig.method](paramValue);
78+
const result = await result$.toPromise();
79+
this.updateBreadcrumbLabel(currentUrl, result);
80+
} catch (error) {
81+
console.warn('Async breadcrumb load failed:', error);
82+
}
83+
}, 0);
84+
85+
return loadingLabel;
6186
}
62-
if (route.routeConfig.path?.startsWith(':')) {
63-
const paramName = route.routeConfig.path.slice(1);
64-
return route.params[paramName] ?? paramName;
87+
const breadcrumbData = data?.['breadcrumb'];
88+
89+
const conditions: [boolean, () => string][] = [
90+
[
91+
typeof breadcrumbData === 'function',
92+
() => breadcrumbData(route.data, route.paramMap, route),
93+
],
94+
[typeof breadcrumbData === 'string', () => breadcrumbData],
95+
];
96+
97+
for (const [condition, action] of conditions) {
98+
if (condition) return action();
6599
}
100+
66101
return this._toTitleCase(path);
67102
}
68-
103+
updateBreadcrumbLabel(url: string, newLabel: string): void {
104+
const currentBreadcrumbs = this.breadcrumbs$.getValue();
105+
const index = currentBreadcrumbs.findIndex(bc => bc.url === url);
106+
if (index !== -1) {
107+
currentBreadcrumbs[index] = {
108+
...currentBreadcrumbs[index],
109+
label: newLabel,
110+
};
111+
this.breadcrumbs$.next([...currentBreadcrumbs]);
112+
}
113+
}
69114
get breadcrumbs() {
70115
return this.breadcrumbs$.asObservable();
71116
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ 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';
26+
import {UserService} from '@project-lib/components/breadcrumb/breadcrumb-demo/user/user.service';
27+
import {TitleService} from '@project-lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.service';
2628

2729
@NgModule({
2830
declarations: [AppComponent],
@@ -49,6 +51,8 @@ import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.componen
4951
TranslateStore,
5052
SystemStoreFacadeService,
5153
EnvAdapterService,
54+
UserService,
55+
TitleService,
5256
ApiService,
5357
{
5458
provide: APP_CONFIG,

0 commit comments

Comments
 (0)