Skip to content

Commit 3d85028

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

File tree

17 files changed

+254
-153
lines changed

17 files changed

+254
-153
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: 51 additions & 14 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,54 @@ 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;
64+
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;
5573

56-
if (typeof breadcrumbData === 'function') {
57-
return breadcrumbData(route.data, route.paramMap, route);
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;
5886
}
87+
const breadcrumbData = data?.['breadcrumb'];
88+
5989
if (typeof breadcrumbData === 'string') {
6090
return breadcrumbData;
6191
}
62-
if (route.routeConfig.path?.startsWith(':')) {
63-
const paramName = route.routeConfig.path.slice(1);
64-
return route.params[paramName] ?? paramName;
65-
}
92+
6693
return this._toTitleCase(path);
6794
}
68-
95+
updateBreadcrumbLabel(url: string, newLabel: string): void {
96+
const currentBreadcrumbs = this.breadcrumbs$.getValue();
97+
const index = currentBreadcrumbs.findIndex(bc => bc.url === url);
98+
if (index !== -1) {
99+
currentBreadcrumbs[index] = {
100+
...currentBreadcrumbs[index],
101+
label: newLabel,
102+
};
103+
this.breadcrumbs$.next([...currentBreadcrumbs]);
104+
}
105+
}
69106
get breadcrumbs() {
70107
return this.breadcrumbs$.asObservable();
71108
}

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,

projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.html

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ <h3>1.Basic Breadcrumb</h3>
1919
<pre>{{ basicCode }}</pre>
2020
</div>
2121

22-
<h3>2.Dynamic Handling Breadcrumb</h3>
22+
<h3>2. Dynamic Handling Breadcrumb</h3>
23+
<p>
24+
This example demonstrates how breadcrumbs can be dynamically generated and
25+
updated based on route parameters and asynchronous data fetching from
26+
services. Instead of relying on blocking resolvers, the breadcrumb labels
27+
are initially displayed using fallback or loading placeholders (e.g.,
28+
"Loading user...") and are automatically updated when the actual data
29+
becomes available from the API or service. This approach ensures smooth
30+
navigation and improves user experience by preventing full page loading
31+
delays while keeping the breadcrumb context accurate and dynamic.
32+
</p>
2333

2434
<ul class="breadcrumb">
2535
<li>Main</li>
@@ -59,24 +69,25 @@ <h3>Routing Code</h3>
5969
</div>
6070

6171
<div class="code-block">
62-
<h3>Resolver Code</h3>
72+
<h3>Service Code</h3>
6373
<span
6474
class="copy-icon"
65-
(click)="copyCode(resolverCode)"
66-
title="Copy Resolver Code"
75+
(click)="copyCode(serviceCode)"
76+
title="Copy Service Code"
6777
>📋</span
6878
>
69-
<pre>{{ resolverCode }}</pre>
79+
<pre>{{ serviceCode }}</pre>
7080
</div>
81+
7182
<div class="code-block">
72-
<h3>Service Code</h3>
83+
<h3>Async Breadcrumb Logic</h3>
7384
<span
7485
class="copy-icon"
75-
(click)="copyCode(serviceCode)"
76-
title="Copy Service Code"
86+
(click)="copyCode(asyncLogicCode)"
87+
title="Copy Async Logic Code"
7788
>📋</span
7889
>
79-
<pre>{{ serviceCode }}</pre>
90+
<pre>{{ asyncLogicCode }}</pre>
8091
</div>
8192
</div>
8293
<h3>3. Breadcrumb Inputs</h3>
@@ -102,10 +113,6 @@ <h3>3. Breadcrumb Inputs</h3>
102113
<td>skipLink</td>
103114
<td>A flag to disable click navigation on a breadcrumb</td>
104115
</tr>
105-
<tr>
106-
<td>resolver</td>
107-
<td>Angular service that pre-fetches data before routing</td>
108-
</tr>
109116
<tr>
110117
<td>separatorClass</td>
111118
<td>Custom class applied to the separator between breadcrumb items</td>

0 commit comments

Comments
 (0)