Skip to content

Commit 3760e45

Browse files
committed
refactor(sidebar-nav): signal inputs, test
1 parent 5a95a43 commit 3760e45

File tree

5 files changed

+90
-80
lines changed

5 files changed

+90
-80
lines changed

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1+
@let grpItem = item();
12
<a (click)="toggleGroup($event)"
2-
[cHtmlAttr]="item.attributes"
3+
[cHtmlAttr]="grpItem?.attributes"
34
class="nav-link nav-group-toggle"
45
href>
5-
<ng-container *ngTemplateOutlet="iconTemplate ; context: {$implicit: item}" />
6-
<ng-container>{{ item.name }}</ng-container>
7-
@if (helper.hasBadge(item)) {
8-
<span [ngClass]="item | cSidebarNavBadge">{{ item.badge.text }}</span>
6+
<ng-container *ngTemplateOutlet="iconTemplate ; context: {$implicit: grpItem}" />
7+
<ng-container>{{ grpItem?.name }}</ng-container>
8+
@if (grpItem?.badge) {
9+
<span [ngClass]="item | cSidebarNavBadge">{{ grpItem?.badge?.text }}</span>
910
}
1011
</a>
1112
<c-sidebar-nav
1213
(@openClose.done)="onAnimationDone($event)"
1314
(@openClose.start)="onAnimationStart($event)"
14-
[@openClose]="open ? 'open' : 'closed'"
15-
[compact]="compact"
16-
[dropdownMode]="dropdownMode"
15+
[@openClose]="open() ? 'open' : 'closed'"
16+
[compact]="compact()"
17+
[dropdownMode]="dropdownMode()"
1718
[groupItems]="true"
18-
[navItems]="navItems"
19-
[ngStyle]="display"
19+
[navItems]="navItems()"
20+
[ngStyle]="display()"
2021
/>
2122

2223
<ng-template #iconTemplate let-item>

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@ describe('SidebarNavGroupComponent', () => {
1111

1212
beforeEach(waitForAsync(() => {
1313
TestBed.configureTestingModule({
14-
imports: [
15-
NoopAnimationsModule,
16-
SidebarNavGroupComponent
17-
],
14+
imports: [NoopAnimationsModule, SidebarNavGroupComponent],
1815
providers: [provideRouter([])]
19-
})
20-
.compileComponents();
16+
}).compileComponents();
2117
}));
2218

2319
beforeEach(() => {
@@ -46,7 +42,7 @@ describe('SidebarNavGroupComponent', () => {
4642
}
4743
]
4844
};
49-
component.item = item;
45+
fixture.componentRef.setInput('item', item);
5046

5147
fixture.detectChanges();
5248
});

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
1-
@for (item of navItemsArray; track item) {
2-
@switch (helper.itemType(item)) {
1+
@for (nItem of navItemsArray(); track nItem) {
2+
@switch (helper.itemType(nItem)) {
33
@case ('group') {
44
<c-sidebar-nav-group
55
#rla="routerLinkActive"
6-
[dropdownMode]="dropdownMode"
7-
[item]="item"
8-
[ngClass]="item | cSidebarNavItemClass"
6+
[dropdownMode]="dropdownMode()"
7+
[item]="nItem"
8+
[ngClass]="nItem | cSidebarNavItemClass"
99
[routerLinkActiveOptions]="{exact: true}"
1010
routerLinkActive="show"
11-
[compact]="compact"
11+
[compact]="compact()"
1212
/>
1313
}
1414
@case ('divider') {
1515
<c-sidebar-nav-divider
16-
[cHtmlAttr]="item.attributes ?? {}"
17-
[item]="item"
18-
[ngClass]="item | cSidebarNavItemClass"
16+
[cHtmlAttr]="nItem.attributes ?? {}"
17+
[item]="nItem"
18+
[ngClass]="nItem | cSidebarNavItemClass"
1919
/>
2020
}
2121
@case ('title') {
2222
<c-sidebar-nav-title
23-
[cHtmlAttr]="item.attributes ?? {}"
24-
[item]="item"
25-
[ngClass]="item | cSidebarNavItemClass"
23+
[cHtmlAttr]="nItem.attributes ?? {}"
24+
[item]="nItem"
25+
[ngClass]="nItem | cSidebarNavItemClass"
2626
/>
2727
}
2828
@case ('label') {
2929
<c-sidebar-nav-label
30-
[item]="item"
31-
[ngClass]="item | cSidebarNavItemClass"
30+
[item]="nItem"
31+
[ngClass]="nItem | cSidebarNavItemClass"
3232
/>
3333
}
3434
@case ('empty') {
@@ -37,8 +37,8 @@
3737
@default {
3838
<c-sidebar-nav-link
3939
(linkClick)="hideMobile()"
40-
[item]="item"
41-
[ngClass]="item | cSidebarNavItemClass"
40+
[item]="nItem"
41+
[ngClass]="nItem | cSidebarNavItemClass"
4242
/>
4343
}
4444
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('SidebarNavComponent', () => {
3535
}
3636
}
3737
];
38-
component.navItems = navItems;
38+
fixture.componentRef.setInput('navItems', navItems);
3939

4040
fixture.detectChanges();
4141
});

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

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
33
import {
44
booleanAttribute,
55
Component,
6+
computed,
67
ElementRef,
78
forwardRef,
8-
HostBinding,
99
inject,
10-
Input,
10+
input,
1111
OnChanges,
1212
OnDestroy,
1313
OnInit,
1414
Renderer2,
15+
signal,
1516
SimpleChanges,
16-
ViewChild
17+
viewChild
1718
} from '@angular/core';
1819
import { NavigationEnd, Router, RouterModule } from '@angular/router';
1920
import { Observable, Subscription } from 'rxjs';
@@ -65,7 +66,10 @@ import { IconDirective } from '@coreui/icons-angular';
6566
),
6667
transition('open <=> closed', [animate('.15s ease')])
6768
])
68-
]
69+
],
70+
host: {
71+
'[class]': 'hostClasses()'
72+
}
6973
})
7074
export class SidebarNavGroupComponent implements OnInit, OnDestroy {
7175
readonly #router = inject(Router);
@@ -82,34 +86,36 @@ export class SidebarNavGroupComponent implements OnInit, OnDestroy {
8286
) as Observable<NavigationEnd>;
8387
}
8488

85-
@Input() item: any;
86-
@Input() dropdownMode: 'path' | 'none' | 'close' = 'path';
87-
@Input() show?: boolean;
88-
@Input({ transform: booleanAttribute }) compact?: boolean;
89+
readonly item = input<INavData>();
90+
readonly dropdownMode = input<'path' | 'none' | 'close'>('path');
91+
readonly show = input<boolean>();
92+
readonly compact = input<boolean, unknown>(undefined, { transform: booleanAttribute });
8993

90-
@HostBinding('class')
91-
get hostClasses(): any {
94+
readonly hostClasses = computed(() => {
9295
return {
9396
'nav-group': true,
94-
show: this.open
97+
show: this.open()
9598
};
96-
}
99+
});
97100

98-
@ViewChild(forwardRef(() => SidebarNavComponent), { read: ElementRef }) sidebarNav!: ElementRef;
101+
readonly sidebarNav = viewChild.required(
102+
forwardRef(() => SidebarNavComponent),
103+
{ read: ElementRef }
104+
);
99105

100106
navigationEndObservable: Observable<NavigationEnd>;
101107
navSubscription!: Subscription;
102108
navGroupSubscription!: Subscription;
103109

104-
public open!: boolean;
105-
public navItems: INavData[] = [];
106-
public display: any = { display: 'block' };
110+
readonly open = signal<boolean | undefined>(undefined);
111+
readonly navItems = signal<INavData[]>([]);
112+
readonly display = signal<any>({ display: 'block' });
107113

108114
ngOnInit(): void {
109-
this.navItems = [...this.item.children];
115+
this.navItems.set([...(this.item()?.children ?? [])]);
110116

111117
this.navSubscription = this.navigationEndObservable.subscribe((event: NavigationEnd) => {
112-
if (this.dropdownMode !== 'none') {
118+
if (this.dropdownMode() !== 'none') {
113119
const samePath = this.samePath(event.url);
114120
this.openGroup(samePath);
115121
}
@@ -120,8 +126,12 @@ export class SidebarNavGroupComponent implements OnInit, OnDestroy {
120126
}
121127

122128
this.navGroupSubscription = this.#sidebarNavGroupService.sidebarNavGroupState$.subscribe((next) => {
123-
if (this.dropdownMode === 'close' && next.sidebarNavGroup && next.sidebarNavGroup !== this) {
124-
if (next.sidebarNavGroup.item.url.startsWith(this.item.url)) {
129+
if (this.dropdownMode() === 'close' && next.sidebarNavGroup && next.sidebarNavGroup !== this) {
130+
const url = next.sidebarNavGroup.item()?.url ?? [];
131+
const normUrl = Array.isArray(url) ? this.#router.createUrlTree(url).toString() : url;
132+
const itemUrl = this.item()?.url ?? [];
133+
const normItemUrl = Array.isArray(normUrl) ? this.#router.createUrlTree(normUrl).toString() : normUrl;
134+
if (normUrl.startsWith(normItemUrl)) {
125135
return;
126136
}
127137
if (this.samePath(this.#router.routerState.snapshot.url)) {
@@ -135,7 +145,8 @@ export class SidebarNavGroupComponent implements OnInit, OnDestroy {
135145

136146
samePath(url: string): boolean {
137147
// console.log('item:', this.item.name, this.item.url, 'url:', url);
138-
const itemArray = this.item.url?.split('/');
148+
const itemUrl = this.item()?.url ?? [];
149+
const itemArray = Array.isArray(itemUrl) ? itemUrl : itemUrl.split('/');
139150
const urlArray = url.split('/');
140151
return itemArray?.every((value: string, index: number) => {
141152
// console.log(value === urlArray[index], 'value:', value, 'index:', index, urlArray[index], url);
@@ -144,14 +155,14 @@ export class SidebarNavGroupComponent implements OnInit, OnDestroy {
144155
}
145156

146157
openGroup(open: boolean): void {
147-
this.open = open;
158+
this.open.set(open);
148159
}
149160

150161
toggleGroup($event: any): void {
151162
$event.preventDefault();
152-
this.openGroup(!this.open);
153-
if (this.open) {
154-
this.#sidebarNavGroupService.toggle({ open: this.open, sidebarNavGroup: this });
163+
this.openGroup(!this.open());
164+
if (this.open()) {
165+
this.#sidebarNavGroupService.toggle({ open: this.open(), sidebarNavGroup: this });
155166
}
156167
}
157168

@@ -160,9 +171,9 @@ export class SidebarNavGroupComponent implements OnInit, OnDestroy {
160171
}
161172

162173
onAnimationStart($event: AnimationEvent) {
163-
this.display = { display: 'block' };
174+
this.display.set({ display: 'block' });
164175
setTimeout(() => {
165-
const host = this.sidebarNav?.nativeElement;
176+
const host = this.sidebarNav()?.nativeElement;
166177
if ($event.toState === 'open' && host) {
167178
this.#renderer.setStyle(host, 'height', `${host['scrollHeight']}px`);
168179
}
@@ -171,13 +182,13 @@ export class SidebarNavGroupComponent implements OnInit, OnDestroy {
171182

172183
onAnimationDone($event: AnimationEvent) {
173184
setTimeout(() => {
174-
const host = this.sidebarNav?.nativeElement;
185+
const host = this.sidebarNav()?.nativeElement;
175186
if ($event.toState === 'open' && host) {
176187
this.#renderer.setStyle(host, 'height', 'auto');
177188
}
178189
if ($event.toState === 'closed') {
179190
setTimeout(() => {
180-
this.display = null;
191+
this.display.set(null);
181192
});
182193
}
183194
});
@@ -197,7 +208,11 @@ export class SidebarNavGroupComponent implements OnInit, OnDestroy {
197208
forwardRef(() => SidebarNavGroupComponent),
198209
SidebarNavItemClassPipe,
199210
RouterModule
200-
]
211+
],
212+
host: {
213+
'[class]': 'hostClasses()',
214+
'[attr.role]': 'role()'
215+
}
201216
})
202217
export class SidebarNavComponent implements OnChanges {
203218
readonly sidebar = inject(SidebarComponent, { optional: true });
@@ -207,33 +222,31 @@ export class SidebarNavComponent implements OnChanges {
207222
readonly #hostElement = inject(ElementRef);
208223
readonly #sidebarService = inject(SidebarService);
209224

210-
@Input() navItems?: INavData[] = [];
211-
@Input() dropdownMode: 'path' | 'none' | 'close' = 'path';
212-
@Input({ transform: booleanAttribute }) groupItems?: boolean;
213-
@Input({ transform: booleanAttribute }) compact?: boolean;
225+
readonly navItems = input<INavData[] | undefined>([]);
226+
readonly dropdownMode = input<'path' | 'none' | 'close'>('path');
227+
readonly groupItems = input<boolean, unknown>(undefined, { transform: booleanAttribute });
228+
readonly compact = input<boolean, unknown>(undefined, { transform: booleanAttribute });
229+
readonly role = input('navigation');
214230

215-
@HostBinding('class')
216-
get hostClasses(): any {
231+
readonly hostClasses = computed(() => {
232+
const groupItems = this.groupItems();
217233
return {
218-
'sidebar-nav': !this.groupItems,
219-
'nav-group-items': this.groupItems,
220-
compact: this.groupItems && this.compact
234+
'sidebar-nav': !groupItems,
235+
'nav-group-items': groupItems,
236+
compact: groupItems && this.compact()
221237
};
222-
}
238+
});
223239

224240
// @HostBinding('class.nav-group-items')
225241
// get sidebarNavGroupItemsClass(): boolean {
226242
// return !!this.groupItems;
227243
// }
228244

229-
@HostBinding('attr.role')
230-
@Input()
231-
role = 'navigation';
232-
233-
public navItemsArray: INavData[] = [];
245+
readonly navItemsArray = signal<INavData[]>([]);
234246

235247
public ngOnChanges(changes: SimpleChanges): void {
236-
this.navItemsArray = Array.isArray(this.navItems) ? this.navItems.slice() : [];
248+
const navItems = this.navItems();
249+
this.navItemsArray.set(Array.isArray(navItems) ? navItems.slice() : []);
237250
}
238251

239252
public hideMobile(): void {

0 commit comments

Comments
 (0)