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 2 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {TitleDetails, UserDetails} from './user-title.interface';

export const USERS: UserDetails[] = [
{id: '123', name: 'John Doe', email: 'john.doe123@example.com'},
{id: '124', name: 'Jane Smith', email: 'jane.smith124@example.com'},
];
export const TITLES: TitleDetails[] = [
{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 UserDetails {
id: string;
name: string;
email: string;
}
export interface TitleDetails {
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 {TitleDetails} from '../user-title.interface';

@Component({
selector: 'lib-user-title',
templateUrl: './user-title.component.html',
})
export class UserTitleComponent {
title: TitleDetails;
constructor(private readonly route: ActivatedRoute) {
this.title = this.route.snapshot.data['document'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot} from '@angular/router';
import {Observable} from 'rxjs';
import {TitleService} from './user-title.service';
import {TitleDetails} from '../user-title.interface';

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

resolve(route: ActivatedRouteSnapshot): Observable<TitleDetails> {
const id = route.paramMap.get('id');
console.log(this.titleService.getTitleById(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,14 @@
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {TITLES} from '../mock-data.constants';
import {TitleDetails} from '../user-title.interface';

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

getTitleById(id: string): Observable<TitleDetails> {
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 {UserDetails} 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: UserDetails;

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 {UserDetails} from '../user-title.interface';

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

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

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

getUserById(id: string): Observable<UserDetails> {
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>
</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;
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,41 @@
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, Subject, takeUntil} 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 = [];
@Input() separator = '>';
@Input() maxItems = 8;
@Input() separatorClass = 'separator';
@Input() itemClass = 'breadcrumb-item';

expanded = false;
private destroy$ = new Subject<void>();
constructor(private readonly breadcrumbService: BreadcrumbService) {}
ngOnInit(): void {
this.breadcrumbs$.pipe(takeUntil(this.destroy$)).subscribe(breadcrumbs => {
if (isDevMode()) {
console.log('Breadcrumbs:', breadcrumbs);
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
toggleExpand() {
this.expanded = true;
}
}
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,81 @@
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'];

const conditions: [boolean, () => string][] = [
[
typeof breadcrumbData === 'function',
() => breadcrumbData(route.data, route.paramMap, route),
],
[typeof breadcrumbData === 'string', () => breadcrumbData],
[
route.routeConfig.path?.startsWith(':'),
() => {
const paramName = route.routeConfig.path.slice(1);
return route.params[paramName] ?? paramName;
},
],
];

for (const [condition, action] of conditions) {
if (condition) return action();
}

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
Loading