diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.spec.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.spec.ts
index af79923d..26fd86eb 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.spec.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.spec.ts
@@ -5,4 +5,17 @@ describe('SidebarNavBadgePipe', () => {
const pipe = new SidebarNavBadgePipe();
expect(pipe).toBeTruthy();
});
+
+ it('should add each class of the badge', () => {
+ const pipe = new SidebarNavBadgePipe();
+ const classes = pipe.transform({ badge: { color: 'info', class: 'first second' } });
+ expect(Object.keys(classes).filter((key) => classes[key])).toEqual([
+ 'badge',
+ 'ms-auto',
+ 'badge-sm',
+ 'bg-info',
+ 'first',
+ 'second'
+ ]);
+ });
});
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.ts
index 4a57aad7..70a3b918 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-badge.pipe.ts
@@ -1,5 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core';
+import { classMap } from './sidebar-nav.utils';
+
@Pipe({
name: 'cSidebarNavBadge'
})
@@ -12,7 +14,7 @@ export class SidebarNavBadgePipe implements PipeTransform {
'badge-sm': !badge.size,
[`badge-${badge.size}`]: !!badge.size,
[`bg-${badge.color}`]: !!badge.color,
- [`${badge.class}`]: !!badge.class
+ ...classMap(badge.class)
};
}
}
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.spec.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.spec.ts
index 2e72e837..bf5f2211 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.spec.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.spec.ts
@@ -11,6 +11,11 @@ describe('SidebarNavIconPipe', () => {
expect(pipe.transform({ icon: 'cil-list' })).toEqual({ 'nav-icon': true, 'cil-list': true });
});
+ it('should add each class of the item icon', () => {
+ const pipe = new SidebarNavIconPipe();
+ expect(pipe.transform({ icon: 'fa fa-home' })).toEqual({ 'nav-icon': true, fa: true, 'fa-home': true });
+ });
+
it('should return the nav-icon class without an item', () => {
const pipe = new SidebarNavIconPipe();
const classes = pipe.transform(undefined);
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.ts
index e22c303f..53223bea 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-icon.pipe.ts
@@ -1,14 +1,15 @@
import { Pipe, PipeTransform } from '@angular/core';
+import { classMap } from './sidebar-nav.utils';
+
@Pipe({
name: 'cSidebarNavIcon'
})
export class SidebarNavIconPipe implements PipeTransform {
transform(item: any, args?: any): any {
- const icon = item?.icon;
return {
'nav-icon': true,
- [`${icon}`]: !!icon
+ ...classMap(item?.icon)
};
}
}
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.html b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.html
index 1bc6eb93..66461ae2 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.html
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.html
@@ -3,7 +3,7 @@
[class]="itemClass()"
href="{{labelItem.url}}">
@if (helper.hasIcon(labelItem)) {
-
+
}
{{ labelItem.name }}
@if (helper.hasBadge(labelItem)) {
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.spec.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.spec.ts
index e93bf3a1..ec3abaf8 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.spec.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.spec.ts
@@ -43,14 +43,36 @@ describe('SidebarNavLabelComponent', () => {
it('should set correct itemClass', async () => {
const link = fixture.debugElement.query(By.css('a')).nativeElement;
+ expect(link.classList.contains('nav-link')).toBe(true);
+ expect(link.classList.contains('nav-label')).toBe(true);
expect(link.classList.contains('c-nav-label')).toBe(true);
expect(link.classList.contains('c-active')).toBe(true);
+ expect(link.classList.contains('active')).toBe(false);
item.class = 'custom-class';
fixture.componentRef.setInput('item', { ...item });
await fixture.whenStable();
expect(link.classList.contains('custom-class')).toBe(true);
});
+ it('should set several classes of the item', async () => {
+ fixture.componentRef.setInput('item', {
+ ...item,
+ class: 'first second',
+ icon: 'fa fa-star',
+ label: { class: 'third fourth' }
+ });
+ await fixture.whenStable();
+ const link = fixture.debugElement.query(By.css('a')).nativeElement;
+ expect(link.classList.contains('first')).toBe(true);
+ expect(link.classList.contains('second')).toBe(true);
+ const icon = fixture.debugElement.query(By.css('i')).nativeElement;
+ expect(icon.classList.contains('third')).toBe(true);
+ expect(icon.classList.contains('fourth')).toBe(true);
+ expect(icon.classList.contains('fa')).toBe(true);
+ expect(icon.classList.contains('fa-star')).toBe(true);
+ expect(icon.getAttribute('aria-hidden')).toBe('true');
+ });
+
it('should display label name', () => {
const label = fixture.debugElement.query(By.css('a')).nativeElement;
expect(label.textContent).toBe(item.name);
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.ts
index 39c93e55..42c202b4 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-label.component.ts
@@ -4,6 +4,7 @@ import { HtmlAttributesDirective } from '../../shared';
import { SidebarNavHelper } from './sidebar-nav.service';
import { SidebarNavBadgePipe } from './sidebar-nav-badge.pipe';
import { INavData } from './sidebar-nav';
+import { classMap } from './sidebar-nav.utils';
@Component({
selector: 'c-sidebar-nav-label',
@@ -20,25 +21,20 @@ export class SidebarNavLabelComponent {
*/
readonly item = input({});
- readonly itemClass = computed(() => {
- const classes: Record = {
- 'c-nav-label': true,
- 'c-active': true
- };
- const itemClass = this.item().class;
- if (itemClass) {
- classes[itemClass] = !!itemClass;
- }
- return classes;
- });
+ readonly itemClass = computed(() => ({
+ 'nav-link': true,
+ 'nav-label': true,
+ 'c-nav-label': true,
+ 'c-active': true,
+ ...classMap(this.item().class)
+ }));
readonly labelIconClass = computed(() => {
const item = this.item();
- const iconClasses: Record = this.helper.getIconClass(item);
- const variant = `text-${item.label?.variant}`;
- iconClasses[variant] = !!item.label?.variant;
- const labelClass = item.label?.class ?? '';
- iconClasses[labelClass] = !!labelClass;
- return iconClasses;
+ return {
+ ...this.helper.getIconClass(item),
+ [`text-${item.label?.variant}`]: !!item.label?.variant,
+ ...classMap(item.label?.class)
+ };
});
}
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.spec.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.spec.ts
index 310e9efa..b0c355ef 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.spec.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.spec.ts
@@ -59,8 +59,35 @@ describe('SidebarNavTitleComponent content', () => {
expect(wrapper.getAttribute('data-test')).toBe('x');
});
+ it('should render the name with several classes of the item', async () => {
+ const title = await render({ title: true, name: 'Theme', class: 'first second' });
+ expect(title.textContent).toBe('Theme');
+ expect([...title.classList].sort()).toEqual(['first', 'nav-title', 'second']);
+ });
+
it('should render nothing without a name', async () => {
const title = await render({ title: true });
expect(title.childNodes.length).toBe(0);
+ expect([...title.classList]).toEqual(['nav-title']);
+ });
+
+ it('should replace the name and the class when the item changes', async () => {
+ const fixture = TestBed.createComponent(SidebarNavTitleComponent);
+ fixture.componentRef.setInput('item', { title: true, name: 'One', class: 'first' });
+ await fixture.whenStable();
+ fixture.componentRef.setInput('item', { title: true, name: 'Two', class: 'second' });
+ await fixture.whenStable();
+ const title = fixture.nativeElement as HTMLElement;
+ expect(title.textContent).toBe('Two');
+ expect([...title.classList].sort()).toEqual(['nav-title', 'second']);
+ });
+
+ it('should clear the name when the item loses it', async () => {
+ const fixture = TestBed.createComponent(SidebarNavTitleComponent);
+ fixture.componentRef.setInput('item', { title: true, name: 'One', wrapper: { element: 'span' } });
+ await fixture.whenStable();
+ fixture.componentRef.setInput('item', { title: true });
+ await fixture.whenStable();
+ expect((fixture.nativeElement as HTMLElement).childNodes.length).toBe(0);
});
});
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.ts
index aade91c5..e214156c 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav-title.component.ts
@@ -1,9 +1,13 @@
-import { Component, effect, ElementRef, inject, input, Renderer2 } from '@angular/core';
+import { Component, computed, effect, ElementRef, inject, input, Renderer2 } from '@angular/core';
import { INavData } from './sidebar-nav';
+import { classMap } from './sidebar-nav.utils';
@Component({
selector: 'c-sidebar-nav-title',
- template: ''
+ template: '',
+ host: {
+ '[class]': 'itemClass()'
+ }
})
export class SidebarNavTitleComponent {
readonly #elementRef = inject(ElementRef);
@@ -15,17 +19,20 @@ export class SidebarNavTitleComponent {
*/
readonly item = input();
+ readonly itemClass = computed(() => ({
+ 'nav-title': true,
+ ...classMap(this.item()?.class)
+ }));
+
readonly #itemEffect = effect(() => {
const item = this.item();
+ const nativeElement: HTMLElement = this.#elementRef.nativeElement;
+ for (const child of [...nativeElement.childNodes]) {
+ this.#renderer.removeChild(nativeElement, child);
+ }
if (item?.name) {
- const nativeElement: HTMLElement = this.#elementRef.nativeElement;
const name = this.#renderer.createText(item.name);
- if (item?.class) {
- const classes = item.class;
- this.#renderer.addClass(nativeElement, classes);
- }
-
if (item?.wrapper) {
const wrapper = this.#renderer.createElement(item.wrapper.element);
this.addAttribs(item.wrapper.attributes, wrapper);
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.html b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.html
index 1abdfeac..803ac841 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.html
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.html
@@ -19,7 +19,6 @@
}
@case ('label') {
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.spec.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.spec.ts
index 90d5febf..8c2fb507 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.spec.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.component.spec.ts
@@ -88,16 +88,22 @@ describe('SidebarNavComponent items', () => {
let nav: HTMLElement;
const navItems: INavData[] = [
- { title: true, name: 'Components', class: 'text-primary' },
+ { title: true, name: 'Components', class: 'text-primary small' },
{ divider: true },
{ name: 'Label', url: '/label', label: { variant: 'info' }, icon: 'cil-star' },
{
name: 'Base',
url: '/base',
+ icon: 'fa fa-home',
iconComponent: { content: ['64 64', ''] },
children: [{ name: 'Accordion', url: '/base/accordion' }]
},
- { name: 'Dashboard', url: '/dashboard', icon: 'cil-speedometer', badge: { color: 'info', text: 'NEW' } },
+ {
+ name: 'Dashboard',
+ url: '/dashboard',
+ icon: 'cil-speedometer',
+ badge: { color: 'info', text: 'NEW', class: 'first second' }
+ },
{ name: 'CoreUI', url: 'https://coreui.io', attributes: { target: '_blank' } },
{ name: 'Disabled', url: '/disabled', attributes: { disabled: true } },
{}
@@ -122,7 +128,11 @@ describe('SidebarNavComponent items', () => {
'c-sidebar-nav-link'
]);
expect(nav.querySelector('c-sidebar-nav-title')?.textContent).toBe('Components');
- expect(nav.querySelector('c-sidebar-nav-title')?.classList.contains('nav-title')).toBe(true);
+ expect([...(nav.querySelector('c-sidebar-nav-title')?.classList ?? [])].sort()).toEqual([
+ 'nav-title',
+ 'small',
+ 'text-primary'
+ ]);
expect(nav.querySelector('c-sidebar-nav-label a')?.textContent?.trim()).toBe('Label');
expect(nav.querySelector('c-sidebar-nav-label i')?.classList.contains('text-info')).toBe(true);
});
@@ -130,7 +140,10 @@ describe('SidebarNavComponent items', () => {
it('should render a group with its icon and children', () => {
const group = nav.querySelector('c-sidebar-nav-group') as HTMLElement;
expect(group.querySelector('.nav-group-toggle')?.textContent).toContain('Base');
- expect(group.querySelector('svg')?.getAttribute('viewBox')).toBe('0 0 64 64');
+ const icon = group.querySelector('svg') as SVGElement;
+ expect(icon.getAttribute('viewBox')).toBe('0 0 64 64');
+ expect(icon.classList.contains('fa')).toBe(true);
+ expect(icon.classList.contains('fa-home')).toBe(true);
expect(group.querySelector('c-sidebar-nav-link a')?.getAttribute('href')).toBe('/base/accordion');
});
@@ -142,6 +155,8 @@ describe('SidebarNavComponent items', () => {
const badge = internal.querySelector('.badge') as HTMLElement;
expect(badge.textContent).toBe('NEW');
expect(badge.classList.contains('bg-info')).toBe(true);
+ expect(badge.classList.contains('first')).toBe(true);
+ expect(badge.classList.contains('second')).toBe(true);
expect(external.getAttribute('href')).toBe('https://coreui.io');
expect(external.getAttribute('target')).toBe('_blank');
@@ -150,3 +165,20 @@ describe('SidebarNavComponent items', () => {
expect([...disabled.classList].sort()).toEqual(['btn-link', 'disabled', 'nav-link']);
});
});
+
+describe('SidebarNavComponent title', () => {
+ it('should take the title name and class from the same item when it is mutated in place', async () => {
+ TestBed.configureTestingModule({ providers: [provideRouter([])] });
+ const navItems: INavData[] = [{ title: true, name: 'Theme', class: 'first' }];
+ const fixture = TestBed.createComponent(SidebarNavComponent);
+ fixture.componentRef.setInput('navItems', navItems);
+ await fixture.whenStable();
+ navItems[0].class = 'second';
+ navItems[0].name = 'Renamed';
+ fixture.componentRef.setInput('navItems', [...navItems]);
+ await fixture.whenStable();
+ const title = fixture.nativeElement.querySelector('c-sidebar-nav-title') as HTMLElement;
+ expect(title.textContent).toBe('Theme');
+ expect([...title.classList].sort()).toEqual(['first', 'nav-title']);
+ });
+});
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.spec.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.spec.ts
index 892c3819..11d283d8 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.spec.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.spec.ts
@@ -102,4 +102,8 @@ describe('SidebarNavHelper', () => {
expect(service.getIconClass({ icon: 'icon-ban' })).toEqual(expect.objectContaining({ 'nav-icon': true }));
expect(service.getIconClass({ icon: '' })).toEqual(expect.objectContaining({ 'nav-icon': true }));
});
+
+ it('should return each class of the icon', () => {
+ expect(service.getIconClass({ icon: 'fa fa-ban' })).toEqual({ 'nav-icon': true, fa: true, 'fa-ban': true });
+ });
});
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.ts
index f1d9fcbe..e7721807 100644
--- a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.ts
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { INavData } from './sidebar-nav';
+import { classMap } from './sidebar-nav.utils';
@Injectable()
export abstract class SidebarNavService {
@@ -43,12 +44,9 @@ export class SidebarNavHelper {
public hasIconComponent = (item: INavData) => Boolean(item?.iconComponent);
public getIconClass(item: INavData): any {
- const classes = {
- 'nav-icon': true
+ return {
+ 'nav-icon': true,
+ ...classMap(item.icon)
};
- const icon = item.icon;
- // @ts-ignore
- classes[icon] = this.hasIcon(item);
- return classes;
}
}
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.utils.spec.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.utils.spec.ts
new file mode 100644
index 00000000..0e0b57af
--- /dev/null
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.utils.spec.ts
@@ -0,0 +1,20 @@
+import { classMap } from './sidebar-nav.utils';
+
+describe('classMap', () => {
+ it('should map each class of the string', () => {
+ expect(classMap(' first second ')).toEqual({ first: true, second: true });
+ });
+
+ it('should split on ASCII whitespace only, like the class attribute', () => {
+ expect(classMap('first\tsecond\nthird\u00a0fourth')).toEqual({
+ first: true,
+ second: true,
+ 'third\u00a0fourth': true
+ });
+ });
+
+ it('should return an empty map without classes', () => {
+ expect(classMap(undefined)).toEqual({});
+ expect(classMap('')).toEqual({});
+ });
+});
diff --git a/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.utils.ts b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.utils.ts
new file mode 100644
index 00000000..93b85e85
--- /dev/null
+++ b/projects/coreui-angular/src/lib/sidebar/sidebar-nav/sidebar-nav.utils.ts
@@ -0,0 +1,12 @@
+/**
+ * Maps a whitespace-separated class string to a `[class]` binding object, one key per class.
+ * @param classes the classes, e.g. `item.class` of a nav item
+ * @returns a key set to `true` for each class, empty without classes
+ */
+export const classMap = (classes?: string): Record =>
+ Object.fromEntries(
+ (classes ?? '')
+ .split(/[\t\n\f\r ]+/)
+ .filter(Boolean)
+ .map((name) => [name, true])
+ );