Skip to content

feat(arc): implement breadcrumb feature #128

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const USERS = [
{id: '123', name: 'John Doe', email: 'john.doe123@example.com'},
{id: '124', name: 'Jane Smith', email: 'jane.smith124@example.com'},
];
export const TITLES = [
{id: '1', title: 'Contract.pdf'},
{id: '2', title: 'Appointment.pdf'},
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use types here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done mam

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface User {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user model already exists in the library

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done mam,i changed the name

id: string;
name: string;
email: string;
}
export interface Title {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use name id model in library core

id: string;
title: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="container mt-4">
<h2>Documentation</h2>
<p><strong>ID:</strong>{{ title?.id }}</p>
<p><strong>Title:</strong>{{ title?.title }}</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Title} from '../user-title.interface';

@Component({
selector: 'lib-user-title',
templateUrl: './user-title.component.html',
})
export class UserTitleComponent {
title: Title;
constructor(private readonly route: ActivatedRoute) {
this.title = this.route.snapshot.data['document'];
Copy link
Preview

Copilot AI Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component is accessing 'document' from route data while the resolver likely provides the title data under a different key (e.g., 'title'). Please ensure the data key used in the resolver and the component are consistent.

Suggested change
this.title = this.route.snapshot.data['document'];
this.title = this.route.snapshot.data['title'];

Copilot uses AI. Check for mistakes.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {TitleService} from './user-title.service';
import {Title} from '../user-title.interface';

@Injectable()
export class TitleResolver {
constructor(private readonly titleService: TitleService) {}

resolve(route: ActivatedRouteSnapshot): Observable<Title> {
const id = route.paramMap.get('id');
return this.titleService.getTitleById(id);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use linter

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {TITLES} from '../mock-data.constants';

@Injectable()
export class TitleService {
private readonly titles = TITLES;

getTitleById(id: string): Observable<any> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove any

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done !

const title = this.titles.find(u => u.id === id);
return of(title);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="container mt-4">
<h2>User Details</h2>
<p><strong>ID:</strong> {{ user?.id }}</p>
<p><strong>Name:</strong> {{ user?.name }}</p>
<p><strong>Email:</strong> {{ user?.email }}</p>

<router-outlet></router-outlet>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {CommonModule} from '@angular/common';
import {Component} from '@angular/core';
import {ActivatedRoute, RouterModule} from '@angular/router';
import {User} from '../user-title.interface';
import {UserResolver} from './user.resolver';

@Component({
selector: 'lib-user',
standalone: true,
templateUrl: './user.component.html',
imports: [CommonModule, RouterModule],
})
export class UserComponent {
user: User;

constructor(private readonly route: ActivatedRoute) {
this.user = this.route.snapshot.data['user'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {UserService} from './user.service';
import {User} from '../user-title.interface';

@Injectable()
export class UserResolver {
constructor(private readonly userService: UserService) {}

resolve(route: ActivatedRouteSnapshot): Observable<User> {
const id = route.paramMap.get('id');
return this.userService.getUserById(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {USERS} from '../mock-data.constants';

@Injectable()
export class UserService {
private readonly users = USERS;

getUserById(id: string): Observable<any> {
const user = this.users.find(u => u.id === id);
return of(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<nav aria-label="breadcrumb" *ngIf="breadcrumbs$ | async as breadcrumbs">
<ul class="breadcrumb">
<ng-container
*ngIf="breadcrumbs.length > maxItems && !expanded; else fullBreadcrumb"
>
<li class="breadcrumb-item">
<ng-container *ngIf="!breadcrumbs[0].skipLink; else noLinkFirst">
<a [routerLink]="breadcrumbs[0].url">{{ breadcrumbs[0].label }}</a>
</ng-container>
<ng-template #noLinkFirst>
<span>{{ breadcrumbs[0].label }}</span>
</ng-template>
</li>
<span class="separator">{{ separator }}</span>

<li class="breadcrumb-item clickable" (click)="toggleExpand()">...</li>
<span class="separator">{{ separator }}</span>

<li class="breadcrumb-item" [class.active]="true">
<ng-container
*ngIf="!breadcrumbs[breadcrumbs.length - 1].skipLink; else noLinkLast"
>
<a [routerLink]="breadcrumbs[breadcrumbs.length - 1].url">
{{ breadcrumbs[breadcrumbs.length - 1].label }}
</a>
</ng-container>
<ng-template #noLinkLast>
<span>{{ breadcrumbs[breadcrumbs.length - 1].label }}</span>
</ng-template>
</li>
</ng-container>

<ng-template #fullBreadcrumb>
<ng-container *ngFor="let breadcrumb of breadcrumbs; let last = last">
<li class="breadcrumb-item" [class.active]="last">
<ng-container *ngIf="!breadcrumb.skipLink && !last; else noLink">
<a [routerLink]="breadcrumb.url">{{ breadcrumb.label }}</a>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if a breacrumb label is too long? Shouldn't we trim it ?

</ng-container>
<ng-template #noLink>
<span>{{ breadcrumb.label }}</span>
</ng-template>
</li>
<span *ngIf="!last" class="separator">{{ separator }}</span>
</ng-container>
</ng-template>
</ul>
</nav>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.breadcrumb {
display: flex;
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
background-color: transparent;

.breadcrumb-item {
display: flex;
align-items: center;
font-size: 14px;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use rem for responsive design?

color: #555;

a {
color: #007bff;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}

&.active {
font-weight: 600;
color: #333;
}
&.clickable {
cursor: pointer;
color: #007bff;
text-decoration: underline;

&:hover {
color: #0056b3;
}
}
}

.separator {
margin: 0 6px;
color: #aaa;
font-size: 14px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Component, Input, isDevMode, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common';
import {Breadcrumb} from './breadcrumb.interface';
import {BreadcrumbService} from './breadcrumb.service';
import {Observable} from 'rxjs';
import {RouterModule} from '@angular/router';

@Component({
selector: 'app-breadcrumb',
templateUrl: './breadcrumb.component.html',
standalone: true,
imports: [CommonModule, RouterModule],
styleUrls: ['./breadcrumb.component.scss'],
})
export class BreadcrumbComponent implements OnInit {
breadcrumbs$: Observable<Breadcrumb[]> = this.breadcrumbService.breadcrumbs;

@Input() staticBreadcrumbs = [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this being used in component?

@Input() separator = '>';
@Input() maxItems = 8;
@Input() separatorClass = 'separator';
@Input() itemClass = 'breadcrumb-item';

expanded = false;
constructor(private readonly breadcrumbService: BreadcrumbService) {}
ngOnInit(): void {
this.breadcrumbs$.subscribe(breadcrumbs => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destroy observable for all the observables

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

if (isDevMode()) {
console.log('Breadcrumbs:', breadcrumbs);
}
});
}
toggleExpand() {
this.expanded = true;
Copy link
Preview

Copilot AI Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toggleExpand method always sets 'expanded' to true instead of toggling its state, which prevents collapsing the expanded breadcrumb view. Consider updating it to: this.expanded = !this.expanded;

Suggested change
this.expanded = true;
this.expanded = !this.expanded;

Copilot uses AI. Check for mistakes.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Breadcrumb {
label: string;
url: string;
skipLink?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, NavigationEnd, Router} from '@angular/router';
import {BehaviorSubject} from 'rxjs';
import {filter} from 'rxjs/operators';
import {Breadcrumb} from './breadcrumb.interface';

@Injectable({providedIn: 'root'})
export class BreadcrumbService {
private readonly breadcrumbs$ = new BehaviorSubject<Breadcrumb[]>([]);

constructor(private readonly router: Router) {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
const root = this.router.routerState.snapshot.root;
const breadcrumbs = this.buildBreadcrumbs(root);
this.breadcrumbs$.next(breadcrumbs);
});
}

private buildBreadcrumbs(
route: ActivatedRouteSnapshot,
url = '',
breadcrumbs: Breadcrumb[] = [],
): Breadcrumb[] {
if (!route.routeConfig) {
return route.firstChild
? this.buildBreadcrumbs(route.firstChild, url, breadcrumbs)
: breadcrumbs;
}

let path = route.routeConfig.path || '';
Object.keys(route.params).forEach(key => {
path = path.replace(`:${key}`, route.params[key]);
});
const nextUrl = path ? `${url}/${path}` : url;
const label = this._resolveLabel(route, path);
const skipLink = route.routeConfig.data?.['skipLink'] ?? false;

if (label) {
breadcrumbs.push({label, url: nextUrl, skipLink});
}

return route.firstChild
? this.buildBreadcrumbs(route.firstChild, nextUrl, breadcrumbs)
: breadcrumbs;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modify this function to keep it clean, readable and remove if else if

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done mam


private _toTitleCase(str: string): string {
return str.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
}

private _resolveLabel(route: ActivatedRouteSnapshot, path: string): string {
const breadcrumbData = route.routeConfig.data?.['breadcrumb'];

if (typeof breadcrumbData === 'function') {
return breadcrumbData(route.data, route.paramMap, route);
}
if (typeof breadcrumbData === 'string') {
return breadcrumbData;
}
if (route.routeConfig.path?.startsWith(':')) {
const paramName = route.routeConfig.path.slice(1);
return route.params[paramName] ?? paramName;
}
return this._toTitleCase(path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove so many ifs, use array instead

}

get breadcrumbs() {
return this.breadcrumbs$.asObservable();
}
}
1 change: 1 addition & 0 deletions projects/arc-lib/src/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './gantt/gantt.module';
export * from './selector/select.module';
export * from './resize/resize.module';
export * from './list/list.component';
export * from './breadcrumb/breadcrumb.component';
1 change: 1 addition & 0 deletions projects/arc/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const routes: Routes = [
{
path: 'main',
loadChildren: () => import('./main/main.module').then(m => m.MainModule),
data: {skipLink: true},
canActivate: [AuthGuard],
},
{
Expand Down
3 changes: 2 additions & 1 deletion projects/arc/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {environment} from '../environments/environment';
import {ThemeModule} from '@project-lib/theme/theme.module';
import {OverlayModule} from '@angular/cdk/overlay';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {GanttModule} from '@project-lib/components/index';
import {BreadcrumbComponent, GanttModule} from '@project-lib/components/index';
import {SelectModule} from '@project-lib/components/selector';
import {HeaderComponent} from '@project-lib/components/header/header.component';
import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.component';
Expand All @@ -40,6 +40,7 @@ import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.componen
BrowserAnimationsModule,
HeaderComponent,
SidebarComponent,
BreadcrumbComponent,
Copy link
Preview

Copilot AI Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BreadcrumbComponent is declared in the NgModule but it's defined as a standalone component. It should be added to the imports array instead of declarations.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve this

Copy link
Preview

Copilot AI Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since BreadcrumbComponent is declared as a standalone component, it should be added to the NgModule's imports instead of declarations to avoid declaration conflicts.

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vaibhavbhalla2505

To move the BreadcrumbComponent from the declarations array to the imports array in the app.module.ts file, you can follow these steps:

  1. Open app.module.ts: Locate and open the file at projects/arc/src/app/app.module.ts in your code editor.

  2. Find the Arrays: Look for the declarations and imports arrays in the @NgModule decorator.

  3. Remove from Declarations: Remove the BreadcrumbComponent entry from the declarations array.

  4. Add to Imports: Add BreadcrumbComponent to the imports array.

Here’s how the modified code should look:

import { NgModule } from '@angular/core';
// Other imports...
import { BreadcrumbComponent } from '@project-lib/components/breadcrumb/breadcrumb.component';

@NgModule({
  declarations: [
    // Other components...
    HeaderComponent,
    SidebarComponent,
    // Remove BreadcrumbComponent from here
  ],
  imports: [
    BrowserAnimationsModule,
    HeaderComponent,
    SidebarComponent,
    BreadcrumbComponent,  // Add BreadcrumbComponent here
  ],
  // Other configurations...
})
export class AppModule { }

By following these steps, you'll properly integrate the standalone BreadcrumbComponent into your Angular module without causing declaration conflicts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sir ,its already in imports array

@NgModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
LocalizationModule,
CoreModule,
ThemeModule.forRoot('default'),
OverlayModule,
SelectModule,
GanttModule,
BrowserAnimationsModule,
HeaderComponent,
SidebarComponent,
BreadcrumbComponent,
],
providers: [
TranslationService,
TranslateService,
IconPacksManagerService,
TranslateStore,
SystemStoreFacadeService,
EnvAdapterService,
ApiService,
{
provide: APP_CONFIG,
useValue: environment,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

],
providers: [
TranslationService,
Expand Down
Loading