Skip to content

Commit 8f7d83d

Browse files
committed
refactor(sidebar-nav-link): signal inputs, test
1 parent 6d21d97 commit 8f7d83d

File tree

3 files changed

+75
-59
lines changed

3 files changed

+75
-59
lines changed

projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-link.component.html

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,45 @@
1+
@let linkItem = item() ?? {};
2+
13
@switch (linkType) {
24
@case ('disabled') {
3-
<a [cHtmlAttr]="item.attributes ?? {}" [ngClass]="item | cSidebarNavLink">
4-
<ng-container *ngTemplateOutlet="iconTemplate; context: {$implicit: item}" />
5-
<c-sidebar-nav-link-content [item]="item" />
6-
@if (item.badge) {
7-
<span [ngClass]="item | cSidebarNavBadge">{{ item.badge?.text }}</span>
5+
<a [cHtmlAttr]="linkItem.attributes ?? {}" [ngClass]="linkItem | cSidebarNavLink">
6+
<ng-container *ngTemplateOutlet="iconTemplate; context: {$implicit: linkItem}" />
7+
<c-sidebar-nav-link-content [item]="linkItem" />
8+
@if (linkItem.badge) {
9+
<span [ngClass]="linkItem | cSidebarNavBadge">{{ linkItem.badge?.text }}</span>
810
}
911
</a>
1012
}
1113
@case ('external') {
12-
<a (click)="linkClicked()" [cHtmlAttr]="item.attributes ?? {}" [href]="href" [ngClass]="item | cSidebarNavLink">
13-
<ng-container *ngTemplateOutlet="iconTemplate; context: {$implicit: item}" />
14-
<c-sidebar-nav-link-content [item]="item" />
15-
@if (item.badge) {
16-
<span [ngClass]="item | cSidebarNavBadge">{{ item.badge?.text }}</span>
14+
<a (click)="linkClicked()" [cHtmlAttr]="linkItem.attributes ?? {}" [href]="href" [ngClass]="linkItem | cSidebarNavLink">
15+
<ng-container *ngTemplateOutlet="iconTemplate; context: {$implicit: linkItem}" />
16+
<c-sidebar-nav-link-content [item]="linkItem" />
17+
@if (linkItem.badge) {
18+
<span [ngClass]="linkItem | cSidebarNavBadge">{{ linkItem.badge?.text }}</span>
1719
}
1820
</a>
1921
}
2022
@default {
2123
<a (click)="linkClicked()"
22-
[cHtmlAttr]="item.attributes ?? {}"
23-
[fragment]="item.linkProps?.fragment"
24-
[ngClass]="item | cSidebarNavLink"
25-
[preserveFragment]="item.linkProps?.preserveFragment ?? false"
26-
[queryParamsHandling]="item.linkProps?.queryParamsHandling"
27-
[queryParams]="item.linkProps?.queryParams ?? null"
28-
[replaceUrl]="item.linkProps?.replaceUrl ?? false"
29-
[routerLinkActiveOptions]="item.linkProps?.routerLinkActiveOptions ?? { exact: false }"
30-
[routerLink]="item.url"
31-
[skipLocationChange]="item.linkProps?.skipLocationChange ?? false"
32-
[state]="item.linkProps?.state ?? {}"
33-
[target]="item.attributes?.['target']"
24+
[cHtmlAttr]="linkItem.attributes ?? {}"
25+
[fragment]="linkItem.linkProps?.fragment"
26+
[ngClass]="linkItem | cSidebarNavLink"
27+
[preserveFragment]="linkItem.linkProps?.preserveFragment ?? false"
28+
[queryParamsHandling]="linkItem.linkProps?.queryParamsHandling"
29+
[queryParams]="linkItem.linkProps?.queryParams ?? null"
30+
[replaceUrl]="linkItem.linkProps?.replaceUrl ?? false"
31+
[routerLinkActiveOptions]="linkItem.linkProps?.routerLinkActiveOptions ?? { exact: false }"
32+
[routerLink]="linkItem.url"
33+
[skipLocationChange]="linkItem.linkProps?.skipLocationChange ?? false"
34+
[state]="linkItem.linkProps?.state ?? {}"
35+
[target]="linkItem.attributes?.['target']"
3436
routerLinkActive="active"
3537
>
3638
<!-- [class.active]="linkActive"-->
37-
<ng-container *ngTemplateOutlet="iconTemplate ; context: {$implicit: item}" />
38-
<c-sidebar-nav-link-content [item]="item" />
39-
@if (item.badge) {
40-
<span [ngClass]="item | cSidebarNavBadge">{{ item.badge?.text }}</span>
39+
<ng-container *ngTemplateOutlet="iconTemplate ; context: {$implicit: linkItem}" />
40+
<c-sidebar-nav-link-content [item]="linkItem" />
41+
@if (linkItem.badge) {
42+
<span [ngClass]="linkItem | cSidebarNavBadge">{{ linkItem.badge?.text }}</span>
4143
}
4244
</a>
4345
}
Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2-
import { RouterTestingModule } from '@angular/router/testing';
3-
import { Router } from '@angular/router';
2+
import { provideRouter, Router } from '@angular/router';
43

54
import { SidebarNavLinkComponent } from './sidebar-nav-link.component';
65
import { HtmlAttributesDirective } from '../../shared';
6+
import { By } from '@angular/platform-browser';
77

88
describe('SidebarNavLinkComponent', () => {
99
let component: SidebarNavLinkComponent;
@@ -13,12 +13,9 @@ describe('SidebarNavLinkComponent', () => {
1313

1414
beforeEach(waitForAsync(() => {
1515
TestBed.configureTestingModule({
16-
imports: [RouterTestingModule.withRoutes([]),
17-
HtmlAttributesDirective,
18-
SidebarNavLinkComponent
19-
]
20-
})
21-
.compileComponents();
16+
imports: [HtmlAttributesDirective, SidebarNavLinkComponent],
17+
providers: [provideRouter([])]
18+
}).compileComponents();
2219
}));
2320

2421
beforeEach(() => {
@@ -31,11 +28,11 @@ describe('SidebarNavLinkComponent', () => {
3128
url: '/dashboard',
3229
icon: 'cil-speedometer',
3330
badge: {
34-
variant: 'info',
31+
color: 'info',
3532
text: 'NEW'
3633
}
3734
};
38-
component.item = item;
35+
fixture.componentRef.setInput('item', item);
3936

4037
// router.initialNavigation();
4138
fixture.detectChanges();
@@ -44,4 +41,25 @@ describe('SidebarNavLinkComponent', () => {
4441
it('should create', () => {
4542
expect(component).toBeTruthy();
4643
});
44+
45+
it('should have item with name "Dashboard"', () => {
46+
const link = fixture.debugElement.query(By.css('a')).nativeElement;
47+
expect(link.textContent).toContain('Dashboard');
48+
});
49+
50+
it('should have correct URL', () => {
51+
const link = fixture.debugElement.query(By.css('a')).nativeElement;
52+
expect(link.getAttribute('href')).toBe('/dashboard');
53+
});
54+
55+
it('should have correct icon class', () => {
56+
const icon = fixture.debugElement.query(By.css('.cil-speedometer')).nativeElement;
57+
expect(icon).toBeTruthy();
58+
});
59+
60+
it('should have badge with text "NEW"', () => {
61+
const badge = fixture.debugElement.query(By.css('.badge')).nativeElement;
62+
expect(badge.textContent).toContain('NEW');
63+
expect(badge.classList).toContain('bg-info');
64+
});
4765
});

projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-link.component.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { NgClass, NgTemplateOutlet } from '@angular/common';
2-
import { Component, inject, Input, OnDestroy, OnInit, output } from '@angular/core';
2+
import { Component, inject, input, OnDestroy, OnInit, output } from '@angular/core';
3+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
34
import { NavigationEnd, Router, RouterModule } from '@angular/router';
45
import { Observable, Subscription } from 'rxjs';
56
import { filter } from 'rxjs/operators';
6-
import { IconDirective } from '@coreui/icons-angular';
77

8+
import { IconDirective } from '@coreui/icons-angular';
89
// import {SidebarService} from '../sidebar.service';
910
import { HtmlAttributesDirective } from '../../shared';
1011
import { SidebarNavHelper } from './sidebar-nav.service';
@@ -16,16 +17,17 @@ import { SidebarNavIconPipe } from './sidebar-nav-icon.pipe';
1617
@Component({
1718
selector: 'c-sidebar-nav-link-content',
1819
template: `
19-
@if (true) {
20-
<ng-container>{{ item?.name ?? '' }}</ng-container>
20+
@let itemLinkContent = item();
21+
@if (itemLinkContent) {
22+
<ng-container>{{ itemLinkContent?.name ?? '' }}</ng-container>
2123
}
2224
`,
2325
providers: [SidebarNavHelper]
2426
})
2527
export class SidebarNavLinkContentComponent {
2628
readonly helper = inject(SidebarNavHelper);
2729

28-
@Input() item?: INavData;
30+
readonly item = input<INavData>({});
2931
}
3032

3133
@Component({
@@ -47,16 +49,7 @@ export class SidebarNavLinkContentComponent {
4749
export class SidebarNavLinkComponent implements OnInit, OnDestroy {
4850
readonly router = inject(Router);
4951

50-
protected _item: INavData = {};
51-
52-
@Input()
53-
set item(item: INavData) {
54-
this._item = JSON.parse(JSON.stringify(item));
55-
}
56-
57-
get item(): INavData {
58-
return this._item;
59-
}
52+
readonly item = input<INavData>();
6053

6154
readonly linkClick = output();
6255

@@ -74,17 +67,19 @@ export class SidebarNavLinkComponent implements OnInit, OnDestroy {
7467
this.navigationEndObservable = router.events.pipe(
7568
filter((event) => {
7669
return event instanceof NavigationEnd;
77-
})
70+
}),
71+
takeUntilDestroyed()
7872
) as Observable<NavigationEnd>;
7973
}
8074

8175
ngOnInit(): void {
76+
const item = this.item() ?? {};
8277
this.url =
83-
typeof this.item.url === 'string'
84-
? this.item.url
85-
: this.router.serializeUrl(this.router.createUrlTree((this.item.url as any[]) ?? ['']));
78+
typeof item.url === 'string'
79+
? item.url
80+
: this.router.serializeUrl(this.router.createUrlTree((item.url as any[]) ?? ['']));
8681
this.linkType = this.getLinkType();
87-
this.href = this.isDisabled() ? '' : this.item.href || this.url;
82+
this.href = this.isDisabled() ? '' : item.href || this.url;
8883
this.linkActive = this.router.url.split(/[?#(;]/)[0] === this.href.split(/[?#(;]/)[0];
8984
this.navSubscription = this.navigationEndObservable.subscribe((event) => {
9085
const itemUrlArray = this.href.split(/[?#(;]/)[0].split('/');
@@ -102,12 +97,13 @@ export class SidebarNavLinkComponent implements OnInit, OnDestroy {
10297
}
10398

10499
public isDisabled(): boolean {
105-
return this.item?.attributes?.['disabled'];
100+
return this.item()?.attributes?.['disabled'];
106101
}
107102

108103
public isExternalLink(): boolean {
109-
const linkPath = Array.isArray(this.item.url) ? this.item.url[0] : this.item.url;
110-
return !!this.item.href || linkPath?.substring(0, 4) === 'http';
104+
const item = this.item() ?? {};
105+
const linkPath = Array.isArray(item.url) ? item.url[0] : item.url;
106+
return !!item.href || linkPath?.substring(0, 4) === 'http';
111107
}
112108

113109
linkClicked(): void {

0 commit comments

Comments
 (0)