Skip to content

Commit ccb56b4

Browse files
committed
refactor(nav): signal inputs, host bindings, cleanup, tests
1 parent f95a237 commit ccb56b4

File tree

4 files changed

+192
-56
lines changed

4 files changed

+192
-56
lines changed
Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,85 @@
11
import { NavLinkDirective } from './nav-link.directive';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { Component, ComponentRef, DebugElement, input } from '@angular/core';
4+
import { By } from '@angular/platform-browser';
5+
6+
@Component({
7+
template: '<a cNavLink [active]="active()" [disabled]="disabled()">test</a>',
8+
imports: [NavLinkDirective]
9+
})
10+
class TestComponent {
11+
readonly active = input(false);
12+
readonly disabled = input(false);
13+
}
214

315
describe('NavLinkDirective', () => {
16+
let fixture: ComponentFixture<TestComponent>;
17+
let component: TestComponent;
18+
let componentRef: ComponentRef<TestComponent>;
19+
let debugElement: DebugElement;
20+
21+
beforeEach(() => {
22+
TestBed.configureTestingModule({
23+
imports: [TestComponent]
24+
}).compileComponents();
25+
26+
fixture = TestBed.createComponent(TestComponent);
27+
component = fixture.componentInstance;
28+
componentRef = fixture.componentRef;
29+
debugElement = fixture.debugElement.query(By.directive(NavLinkDirective));
30+
fixture.detectChanges();
31+
});
32+
433
it('should create an instance', () => {
5-
const directive = new NavLinkDirective();
6-
expect(directive).toBeTruthy();
34+
TestBed.runInInjectionContext(() => {
35+
const directive = new NavLinkDirective();
36+
expect(directive).toBeTruthy();
37+
});
38+
});
39+
40+
it('should have css classes', () => {
41+
expect(debugElement.nativeElement).toHaveClass('nav-link');
42+
});
43+
44+
it('should have css classes for active', () => {
45+
expect(debugElement.nativeElement).not.toHaveClass('active');
46+
componentRef.setInput('active', true);
47+
fixture.detectChanges();
48+
expect(debugElement.nativeElement).toHaveClass('active');
49+
componentRef.setInput('active', false);
50+
fixture.detectChanges();
51+
expect(debugElement.nativeElement).not.toHaveClass('active');
52+
});
53+
54+
it('should have css classes for disabled', () => {
55+
expect(debugElement.nativeElement).not.toHaveClass('disabled');
56+
componentRef.setInput('disabled', true);
57+
fixture.detectChanges();
58+
expect(debugElement.nativeElement).toHaveClass('disabled');
59+
componentRef.setInput('disabled', false);
60+
fixture.detectChanges();
61+
expect(debugElement.nativeElement).not.toHaveClass('disabled');
62+
});
63+
64+
it('should have aria-* attr for active', () => {
65+
expect(debugElement.nativeElement.getAttribute('aria-current')).not.toBe('page');
66+
componentRef.setInput('active', true);
67+
fixture.detectChanges();
68+
expect(debugElement.nativeElement.getAttribute('aria-current')).toBe('page');
69+
});
70+
71+
it('should have attributes for disabled', () => {
72+
expect(debugElement.nativeElement.getAttribute('disabled')).toBeNull();
73+
expect(debugElement.nativeElement.getAttribute('aria-disabled')).not.toBeTruthy();
74+
expect(debugElement.nativeElement.getAttribute('tabindex')).not.toBe('-1');
75+
expect(debugElement.nativeElement.style.cursor).toBe('pointer');
76+
componentRef.setInput('disabled', true);
77+
fixture.detectChanges();
78+
expect(debugElement.nativeElement.getAttribute('disabled')).not.toBeNull();
79+
expect(debugElement.nativeElement.getAttribute('aria-disabled')).toBeTruthy();
80+
expect(debugElement.nativeElement.getAttribute('tabindex')).toBe('-1');
81+
expect(debugElement.nativeElement.style.cursor).not.toBe('pointer');
82+
componentRef.setInput('disabled', false);
83+
fixture.detectChanges();
784
});
885
});
Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,57 @@
1-
import { booleanAttribute, Directive, HostBinding, Input } from '@angular/core';
1+
import { booleanAttribute, computed, Directive, effect, input } from '@angular/core';
22

33
@Directive({
4-
selector: '[cNavLink]'
4+
selector: '[cNavLink]',
5+
host: {
6+
'[class]': 'hostClasses()',
7+
'[attr.aria-current]': 'ariaCurrent()',
8+
'[attr.aria-disabled]': 'ariaDisabled',
9+
'[attr.disabled]': 'attrDisabled',
10+
'[attr.tabindex]': 'attrTabindex',
11+
'[style.cursor]': 'styleCursor'
12+
}
513
})
614
export class NavLinkDirective {
715
/**
816
* Sets .nav-link class to the host. [docs]
9-
* @type boolean
1017
* @default true
1118
*/
12-
@Input({ transform: booleanAttribute }) cNavLink: string | boolean = true;
19+
readonly cNavLink = input(true, { transform: booleanAttribute });
1320

1421
/**
1522
* Toggle the active state for the component. [docs]
16-
* @type boolean
23+
* @default undefined
1724
*/
18-
@Input() active?: boolean;
25+
readonly active = input<boolean>();
26+
1927
/**
2028
* Set disabled attr for the host element. [docs]
21-
* @type boolean
29+
* @default false
2230
*/
23-
@Input({ transform: booleanAttribute }) disabled: string | boolean = false;
24-
25-
@HostBinding('attr.aria-current')
26-
get ariaCurrent(): string | null {
27-
return this.active ? 'page' : null;
28-
}
29-
30-
@HostBinding('attr.aria-disabled')
31-
get isDisabled(): boolean | null {
32-
return <boolean>this.disabled || null;
33-
}
34-
35-
@HostBinding('attr.disabled')
36-
get attrDisabled() {
37-
return this.disabled ? '' : null;
38-
}
39-
40-
@HostBinding('attr.tabindex')
41-
get getTabindex(): string | null {
42-
return this.disabled ? '-1' : null;
43-
}
44-
45-
@HostBinding('style.cursor')
46-
get getCursorStyle(): string | null {
47-
return this.disabled ? null : 'pointer';
48-
}
49-
50-
@HostBinding('class')
51-
get hostClasses(): any {
31+
readonly disabled = input(false, { transform: booleanAttribute });
32+
33+
readonly ariaCurrent = computed(() => {
34+
return this.active() ? 'page' : null;
35+
});
36+
37+
ariaDisabled: boolean | null = null;
38+
attrDisabled: boolean | string | null = null;
39+
attrTabindex: '-1' | null = null;
40+
styleCursor: 'pointer' | null = null;
41+
42+
readonly disabledEffect = effect(() => {
43+
const disabled = this.disabled();
44+
this.ariaDisabled = disabled || null;
45+
this.attrDisabled = disabled ? '' : null;
46+
this.attrTabindex = disabled ? '-1' : null;
47+
this.styleCursor = disabled ? null : 'pointer';
48+
});
49+
50+
readonly hostClasses = computed(() => {
5251
return {
53-
'nav-link': this.cNavLink,
54-
disabled: this.disabled,
55-
active: this.active
56-
};
57-
}
52+
'nav-link': this.cNavLink(),
53+
disabled: this.disabled(),
54+
active: this.active()
55+
} as Record<string, boolean>;
56+
});
5857
}

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
22

33
import { NavComponent } from './nav.component';
4+
import { ComponentRef } from '@angular/core';
45

56
describe('NavComponent', () => {
67
let component: NavComponent;
78
let fixture: ComponentFixture<NavComponent>;
9+
let componentRef: ComponentRef<NavComponent>;
810

911
beforeEach(waitForAsync(() => {
1012
TestBed.configureTestingModule({
1113
imports: [NavComponent]
12-
})
13-
.compileComponents();
14+
}).compileComponents();
1415
}));
1516

1617
beforeEach(() => {
1718
fixture = TestBed.createComponent(NavComponent);
1819
component = fixture.componentInstance;
20+
componentRef = fixture.componentRef;
1921
fixture.detectChanges();
2022
});
2123

@@ -26,4 +28,60 @@ describe('NavComponent', () => {
2628
it('should have css classes', () => {
2729
expect(fixture.nativeElement).toHaveClass('nav');
2830
});
31+
32+
it('should have css classes for layout', () => {
33+
componentRef.setInput('layout', 'fill');
34+
fixture.detectChanges();
35+
expect(fixture.nativeElement).toHaveClass('nav-fill');
36+
componentRef.setInput('layout', 'justified');
37+
fixture.detectChanges();
38+
expect(fixture.nativeElement).not.toHaveClass('nav-fill');
39+
expect(fixture.nativeElement).toHaveClass('nav-justified');
40+
componentRef.setInput('layout', undefined);
41+
fixture.detectChanges();
42+
expect(fixture.nativeElement).not.toHaveClass('nav-fill');
43+
expect(fixture.nativeElement).not.toHaveClass('nav-justified');
44+
});
45+
46+
it('should have css classes for variant', () => {
47+
expect(fixture.nativeElement).not.toHaveClass('nav-tabs');
48+
expect(fixture.nativeElement).not.toHaveClass('nav-pills');
49+
expect(fixture.nativeElement).not.toHaveClass('nav-underline');
50+
expect(fixture.nativeElement).not.toHaveClass('nav-underline-border');
51+
52+
componentRef.setInput('variant', 'tabs');
53+
fixture.detectChanges();
54+
expect(fixture.nativeElement).toHaveClass('nav-tabs');
55+
expect(fixture.nativeElement).not.toHaveClass('nav-pills');
56+
expect(fixture.nativeElement).not.toHaveClass('nav-underline');
57+
expect(fixture.nativeElement).not.toHaveClass('nav-underline-border');
58+
59+
componentRef.setInput('variant', 'pills');
60+
fixture.detectChanges();
61+
expect(fixture.nativeElement).not.toHaveClass('nav-tabs');
62+
expect(fixture.nativeElement).toHaveClass('nav-pills');
63+
expect(fixture.nativeElement).not.toHaveClass('nav-underline');
64+
expect(fixture.nativeElement).not.toHaveClass('nav-underline-border');
65+
66+
componentRef.setInput('variant', 'underline');
67+
fixture.detectChanges();
68+
expect(fixture.nativeElement).not.toHaveClass('nav-tabs');
69+
expect(fixture.nativeElement).not.toHaveClass('nav-pills');
70+
expect(fixture.nativeElement).toHaveClass('nav-underline');
71+
expect(fixture.nativeElement).not.toHaveClass('nav-underline-border');
72+
73+
componentRef.setInput('variant', 'underline-border');
74+
fixture.detectChanges();
75+
expect(fixture.nativeElement).not.toHaveClass('nav-tabs');
76+
expect(fixture.nativeElement).not.toHaveClass('nav-pills');
77+
expect(fixture.nativeElement).not.toHaveClass('nav-underline');
78+
expect(fixture.nativeElement).toHaveClass('nav-underline-border');
79+
80+
componentRef.setInput('variant', undefined);
81+
fixture.detectChanges();
82+
expect(fixture.nativeElement).not.toHaveClass('nav-tabs');
83+
expect(fixture.nativeElement).not.toHaveClass('nav-pills');
84+
expect(fixture.nativeElement).not.toHaveClass('nav-underline');
85+
expect(fixture.nativeElement).not.toHaveClass('nav-underline-border');
86+
});
2987
});
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
1-
import { Component, HostBinding, Input } from '@angular/core';
1+
import { Component, computed, input } from '@angular/core';
22

33
@Component({
44
selector: 'c-nav',
55
template: '<ng-content />',
66
styleUrls: ['./nav.component.scss'],
7-
host: { class: 'nav' }
7+
host: { class: 'nav', '[class]': 'hostClasses()' }
88
})
99
export class NavComponent {
1010
/**
1111
* Specify a layout type for component.
12-
* @type {'fill' | 'justified'}
12+
* @default undefined
1313
*/
14-
@Input() layout?: 'fill' | 'justified';
14+
readonly layout = input<'fill' | 'justified'>();
15+
1516
/**
1617
* Set the nav variant to tabs or pills.
17-
* @type 'tabs' | 'pills' | 'underline' | 'underline-border'
18+
* @default undefined
1819
*/
19-
@Input() variant?: '' | 'tabs' | 'pills' | 'underline' | 'underline-border';
20+
readonly variant = input<'tabs' | 'pills' | 'underline' | 'underline-border' | ''>();
2021

21-
@HostBinding('class')
22-
get hostClasses(): any {
22+
readonly hostClasses = computed(() => {
23+
const layout = this.layout();
24+
const variant = this.variant();
2325
return {
2426
nav: true,
25-
[`nav-${this.layout}`]: !!this.layout,
26-
[`nav-${this.variant}`]: !!this.variant
27+
[`nav-${layout}`]: !!layout,
28+
[`nav-${variant}`]: !!variant
2729
};
28-
}
30+
});
2931
}

0 commit comments

Comments
 (0)