Skip to content

Commit fe69aa8

Browse files
committedJan 24, 2025
refactor(breadcrumb): signal inputs, host bindings, cleanup, tests
1 parent 34007f4 commit fe69aa8

8 files changed

+141
-70
lines changed
 

‎projects/coreui-angular/src/lib/breadcrumb/breadcrumb-item/breadcrumb-item.component.html

+12-12
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
@if (!active) {
2-
<a [routerLink]="url"
3-
[cHtmlAttr]="attributes ?? {}"
4-
[target]="attributes?.['target']"
5-
[queryParams]="linkProps?.queryParams ?? null"
6-
[fragment]="linkProps?.fragment"
7-
[queryParamsHandling]="linkProps?.queryParamsHandling ?? null"
8-
[preserveFragment]="linkProps?.preserveFragment ?? false"
9-
[skipLocationChange]="linkProps?.skipLocationChange ?? false"
10-
[replaceUrl]="linkProps?.replaceUrl ?? false"
11-
[state]="linkProps?.state ?? {}"
1+
@if (!active()) {
2+
<a [routerLink]="url()"
3+
[cHtmlAttr]="attributes() ?? {}"
4+
[target]="attributes()?.['target']"
5+
[queryParams]="linkProps()?.queryParams ?? null"
6+
[fragment]="linkProps()?.fragment"
7+
[queryParamsHandling]="linkProps()?.queryParamsHandling ?? null"
8+
[preserveFragment]="linkProps()?.preserveFragment ?? false"
9+
[skipLocationChange]="linkProps()?.skipLocationChange ?? false"
10+
[replaceUrl]="linkProps()?.replaceUrl ?? false"
11+
[state]="linkProps()?.state ?? {}"
1212
>
1313
<ng-container *ngTemplateOutlet="defaultBreadcrumbItemContentTemplate" />
1414
</a>
1515
} @else {
16-
<span [cHtmlAttr]="attributes ?? {}">
16+
<span [cHtmlAttr]="attributes() ?? {}">
1717
<ng-container *ngTemplateOutlet="defaultBreadcrumbItemContentTemplate" />
1818
</span>
1919
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2-
import { RouterTestingModule } from '@angular/router/testing';
32
import { BreadcrumbItemComponent } from './breadcrumb-item.component';
3+
import { provideRouter } from '@angular/router';
4+
import { ComponentRef } from '@angular/core';
45

56
describe('BreadcrumbItemComponent', () => {
67
let component: BreadcrumbItemComponent;
8+
let componentRef: ComponentRef<BreadcrumbItemComponent>;
79
let fixture: ComponentFixture<BreadcrumbItemComponent>;
810

911
beforeEach(async () => {
1012
await TestBed.configureTestingModule({
11-
imports: [RouterTestingModule, BreadcrumbItemComponent]
12-
})
13-
.compileComponents();
13+
imports: [BreadcrumbItemComponent],
14+
providers: [provideRouter([])]
15+
}).compileComponents();
1416
});
1517

1618
beforeEach(() => {
1719
fixture = TestBed.createComponent(BreadcrumbItemComponent);
1820
component = fixture.componentInstance;
21+
componentRef = fixture.componentRef;
1922
fixture.detectChanges();
2023
});
2124

@@ -25,5 +28,22 @@ describe('BreadcrumbItemComponent', () => {
2528

2629
it('should have css classes', () => {
2730
expect(fixture.nativeElement).toHaveClass('breadcrumb-item');
31+
expect(fixture.nativeElement).not.toHaveClass('active');
32+
});
33+
34+
it('should have active class', () => {
35+
componentRef.setInput('active', true);
36+
fixture.detectChanges();
37+
expect(fixture.nativeElement).toHaveClass('active');
38+
});
39+
40+
it('should have aria-current attribute', () => {
41+
expect(fixture.nativeElement.getAttribute('aria-current')).toBeNull();
42+
componentRef.setInput('active', true);
43+
fixture.detectChanges();
44+
expect(fixture.nativeElement.getAttribute('aria-current')).toBe('page');
45+
componentRef.setInput('active', false);
46+
fixture.detectChanges();
47+
expect(fixture.nativeElement.getAttribute('aria-current')).toBeNull();
2848
});
2949
});
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,54 @@
1-
import { booleanAttribute, Component, HostBinding, Input } from '@angular/core';
1+
import { booleanAttribute, Component, computed, input } from '@angular/core';
22
import { NgTemplateOutlet } from '@angular/common';
33
import { RouterModule } from '@angular/router';
44

55
import { HtmlAttributesDirective } from '../../shared';
66
import { INavAttributes, INavLinkProps } from './breadcrumb-item';
77

88
@Component({
9-
selector: 'c-breadcrumb-item',
10-
templateUrl: './breadcrumb-item.component.html',
11-
styleUrls: ['./breadcrumb-item.component.scss'],
12-
imports: [RouterModule, NgTemplateOutlet, HtmlAttributesDirective]
9+
selector: 'c-breadcrumb-item',
10+
templateUrl: './breadcrumb-item.component.html',
11+
styleUrls: ['./breadcrumb-item.component.scss'],
12+
imports: [RouterModule, NgTemplateOutlet, HtmlAttributesDirective],
13+
exportAs: 'breadcrumbItem',
14+
host: {
15+
'[attr.aria-current]': 'ariaCurrent()',
16+
'[class]': 'hostClasses()'
17+
}
1318
})
1419
export class BreadcrumbItemComponent {
15-
1620
/**
1721
* Toggle the active state for the component. [docs]
18-
* @type boolean
22+
* @return boolean
1923
*/
20-
@Input({ transform: booleanAttribute }) active?: boolean;
24+
readonly active = input<boolean, unknown>(undefined, { transform: booleanAttribute });
25+
2126
/**
2227
* The `url` prop for the inner `[routerLink]` directive. [docs]
2328
* @type string
2429
*/
25-
@Input() url?: string | any[];
30+
readonly url = input<string | any[]>();
31+
2632
/**
2733
* Additional html attributes for link. [docs]
2834
* @type INavAttributes
2935
*/
30-
@Input() attributes?: INavAttributes;
36+
readonly attributes = input<INavAttributes>();
37+
3138
/**
3239
* Some `NavigationExtras` props for the inner `[routerLink]` directive and `routerLinkActiveOptions`. [docs]
3340
* @type INavLinkProps
3441
*/
35-
@Input() linkProps?: INavLinkProps;
42+
readonly linkProps = input<INavLinkProps>();
3643

37-
@HostBinding('attr.aria-current') get ariaCurrent(): string | null {
38-
return this.active ? 'page' : null;
39-
}
44+
readonly ariaCurrent = computed((): string | null => {
45+
return this.active() ? 'page' : null;
46+
});
4047

41-
@HostBinding('class')
42-
get hostClasses(): any {
48+
readonly hostClasses = computed(() => {
4349
return {
4450
'breadcrumb-item': true,
45-
active: this.active
46-
};
47-
}
51+
active: this.active()
52+
} as Record<string, boolean>;
53+
});
4854
}

‎projects/coreui-angular/src/lib/breadcrumb/breadcrumb-item/breadcrumb-item.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface IBreadcrumbItem {
66
attributes?: INavAttributes;
77
linkProps?: INavLinkProps;
88
class?: string;
9+
queryParams?: { [key: string]: any };
910
}
1011

1112
export { INavAttributes, INavLinkProps, IBreadcrumbItem };
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,72 @@
1+
import { ComponentRef } from '@angular/core';
12
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2-
import { provideRouter, Router } from '@angular/router';
3+
import { provideRouter, Route } from '@angular/router';
4+
import { RouterTestingHarness } from '@angular/router/testing';
5+
import { take } from 'rxjs';
36

47
import { BreadcrumbRouterComponent } from './breadcrumb-router.component';
58
import { BreadcrumbRouterService } from './breadcrumb-router.service';
69

710
describe('BreadcrumbComponent', () => {
811
let component: BreadcrumbRouterComponent;
12+
let componentRef: ComponentRef<BreadcrumbRouterComponent>;
913
let fixture: ComponentFixture<BreadcrumbRouterComponent>;
10-
let router: Router;
14+
let harness: RouterTestingHarness;
15+
16+
const routes: Route[] = [
17+
{ path: 'home', component: BreadcrumbRouterComponent, data: { title: 'Home' } },
18+
{ path: 'color', component: BreadcrumbRouterComponent, title: 'Color' },
19+
{ path: '', component: BreadcrumbRouterComponent }
20+
];
1121

1222
beforeEach(waitForAsync(() => {
1323
TestBed.configureTestingModule({
14-
imports: [
15-
BreadcrumbRouterComponent
16-
],
17-
providers: [BreadcrumbRouterService, provideRouter([])]
24+
imports: [BreadcrumbRouterComponent],
25+
providers: [BreadcrumbRouterService, provideRouter(routes)]
1826
}).compileComponents();
1927
}));
2028

21-
beforeEach(() => {
29+
beforeEach(async () => {
2230
fixture = TestBed.createComponent(BreadcrumbRouterComponent);
23-
router = TestBed.inject(Router);
2431
component = fixture.componentInstance;
32+
componentRef = fixture.componentRef;
33+
34+
harness = await RouterTestingHarness.create();
2535
fixture.detectChanges();
2636
});
2737

2838
it('should create', () => {
2939
expect(component).toBeTruthy();
3040
});
41+
42+
it('should have breadcrumbs', () => {
43+
expect(component.breadcrumbs).toBeDefined();
44+
});
45+
46+
it('should get breadcrumbs from service', async () => {
47+
const comp = await harness.navigateByUrl('/home');
48+
component.breadcrumbs?.pipe(take(1)).subscribe((breadcrumbs) => {
49+
expect(breadcrumbs).toEqual([{ label: 'Home', url: '/home', queryParams: {} }]);
50+
});
51+
});
52+
it('should get breadcrumbs from service', async () => {
53+
const comp = await harness.navigateByUrl('/color?id=1&test=2');
54+
component.breadcrumbs?.pipe(take(1)).subscribe((breadcrumbs) => {
55+
expect(breadcrumbs).toEqual([{ label: 'Color', url: '/color', queryParams: { id: '1', test: '2' } }]);
56+
});
57+
});
58+
it('should get breadcrumbs from service', async () => {
59+
const comp = await harness.navigateByUrl('/');
60+
component.breadcrumbs?.pipe(take(1)).subscribe((breadcrumbs) => {
61+
expect(breadcrumbs).toEqual([{ label: '', url: '/', queryParams: {} }]);
62+
});
63+
});
64+
65+
it('should emit breadcrumbs on items change', () => {
66+
componentRef.setInput('items', [{ label: 'test' }]);
67+
fixture.detectChanges();
68+
component.breadcrumbs?.pipe(take(1)).subscribe((breadcrumbs) => {
69+
expect(breadcrumbs).toEqual([{ label: 'test' }]);
70+
});
71+
});
3172
});

‎projects/coreui-angular/src/lib/breadcrumb/breadcrumb-router/breadcrumb-router.component.ts

+11-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, inject, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
1+
import { Component, effect, inject, input, OnDestroy, OnInit } from '@angular/core';
22
import { Observable, Observer } from 'rxjs';
33
import { AsyncPipe } from '@angular/common';
44

@@ -12,35 +12,31 @@ import { BreadcrumbItemComponent } from '../breadcrumb-item/breadcrumb-item.comp
1212
templateUrl: './breadcrumb-router.component.html',
1313
imports: [BreadcrumbComponent, BreadcrumbItemComponent, AsyncPipe]
1414
})
15-
export class BreadcrumbRouterComponent implements OnChanges, OnDestroy, OnInit {
15+
export class BreadcrumbRouterComponent implements OnDestroy, OnInit {
1616
readonly service = inject(BreadcrumbRouterService);
1717

1818
/**
1919
* Optional array of IBreadcrumbItem to override default BreadcrumbRouter behavior. [docs]
20-
* @type IBreadcrumbItem[]
20+
* @return IBreadcrumbItem[]
2121
*/
22-
@Input() items?: IBreadcrumbItem[];
22+
readonly items = input<IBreadcrumbItem[]>();
2323
public breadcrumbs: Observable<IBreadcrumbItem[]> | undefined;
2424

2525
ngOnInit(): void {
2626
this.breadcrumbs = this.service.breadcrumbs$;
2727
}
2828

29-
public ngOnChanges(changes: SimpleChanges): void {
30-
if (changes['items']) {
31-
this.setup();
32-
}
33-
}
34-
35-
setup(): void {
36-
if (this.items && this.items.length > 0) {
29+
readonly setup = effect(() => {
30+
const items = this.items();
31+
if (items && items.length > 0) {
3732
this.breadcrumbs = new Observable<IBreadcrumbItem[]>((observer: Observer<IBreadcrumbItem[]>) => {
38-
if (this.items) {
39-
observer.next(this.items);
33+
const itemsValue = this.items();
34+
if (itemsValue) {
35+
observer.next(itemsValue);
4036
}
4137
});
4238
}
43-
}
39+
});
4440

4541
ngOnDestroy(): void {
4642
this.breadcrumbs = undefined;

‎projects/coreui-angular/src/lib/breadcrumb/breadcrumb/breadcrumb.component.spec.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ describe('BreadcrumbComponent', () => {
99
beforeEach(async () => {
1010
await TestBed.configureTestingModule({
1111
imports: [BreadcrumbComponent]
12-
})
13-
.compileComponents();
12+
}).compileComponents();
1413
});
1514

1615
beforeEach(() => {
@@ -26,4 +25,12 @@ describe('BreadcrumbComponent', () => {
2625
it('should have css classes', () => {
2726
expect(fixture.nativeElement).toHaveClass('breadcrumb');
2827
});
28+
29+
it('should have aria-label attribute', () => {
30+
expect(fixture.nativeElement.getAttribute('aria-label')).toBe('breadcrumb');
31+
});
32+
33+
it('should have role attribute', () => {
34+
expect(fixture.nativeElement.getAttribute('role')).toBe('navigation');
35+
});
2936
});
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
import { Component, HostBinding, Input } from '@angular/core';
1+
import { Component, input } from '@angular/core';
22

33
@Component({
44
selector: 'c-breadcrumb',
55
template: '<ng-content />',
6-
host: { class: 'breadcrumb' }
6+
host: {
7+
class: 'breadcrumb',
8+
'[attr.aria-label]': 'ariaLabel()',
9+
'[attr.role]': 'role()'
10+
}
711
})
812
export class BreadcrumbComponent {
913
/**
1014
* Default aria-label for breadcrumb. [docs]
11-
* @type string
15+
* @return string
1216
* @default 'breadcrumb'
1317
*/
14-
@HostBinding('attr.aria-label')
15-
@Input()
16-
ariaLabel = 'breadcrumb';
18+
readonly ariaLabel = input('breadcrumb');
1719

1820
/**
1921
* Default role for breadcrumb. [docs]
20-
* @type string
22+
* @return string
2123
* @default 'navigation'
2224
*/
23-
@HostBinding('attr.role')
24-
@Input()
25-
role = 'navigation';
25+
readonly role = input('navigation');
2626
}

0 commit comments

Comments
 (0)
Please sign in to comment.