Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2c9ae31

Browse files
committedOct 28, 2024
refactor(navbar): input signals, host bindings
1 parent a922612 commit 2c9ae31

File tree

8 files changed

+152
-105
lines changed

8 files changed

+152
-105
lines changed
 
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { NavbarBrandDirective } from './navbar-brand.directive';
2+
import { TestBed } from '@angular/core/testing';
23

34
describe('NavbarBrandDirective', () => {
45
it('should create an instance', () => {
5-
const directive = new NavbarBrandDirective();
6-
expect(directive).toBeTruthy();
6+
TestBed.runInInjectionContext(() => {
7+
const directive = new NavbarBrandDirective();
8+
expect(directive).toBeTruthy();
9+
});
710
});
811
});
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { Directive, HostBinding } from '@angular/core';
1+
import { Directive, input } from '@angular/core';
22

33
@Directive({
44
selector: '[cNavbarBrand]',
5-
standalone: true
5+
standalone: true,
6+
host: { class: 'navbar-brand', '[attr.role]': 'role()' }
67
})
78
export class NavbarBrandDirective {
8-
9-
@HostBinding('class.navbar-brand') navbarBrand = true;
10-
@HostBinding('attr.role') role = 'button';
11-
9+
readonly role = input('button');
1210
}
Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
import { booleanAttribute, Component, HostBinding, Input } from '@angular/core';
1+
import { booleanAttribute, Component, computed, input } from '@angular/core';
22

33
@Component({
44
selector: 'c-navbar-nav',
55
template: '<ng-content />',
66
standalone: true,
7-
host: { class: 'navbar-nav' }
7+
host: { '[class]': 'hostClasses()' }
88
})
99
export class NavbarNavComponent {
1010
/**
1111
* Enable vertical scrolling of a collapsed navbar toggleable contents.
1212
* @type boolean
1313
*/
14-
@Input({ transform: booleanAttribute }) scroll: string | boolean = false;
14+
readonly scroll = input(false, { transform: booleanAttribute });
1515

16-
@HostBinding('class')
17-
get hostClasses(): any {
16+
readonly hostClasses = computed(() => {
1817
return {
1918
'navbar-nav': true,
20-
'navbar-nav-scroll': this.scroll
21-
};
22-
}
19+
'navbar-nav-scroll': this.scroll()
20+
} as Record<string, boolean>;
21+
});
2322
}
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import { Component, HostBinding } from '@angular/core';
1+
import { Component } from '@angular/core';
22

33
@Component({
44
selector: 'c-navbar-text',
55
template: '<ng-content />',
6-
standalone: true
6+
standalone: true,
7+
host: { class: 'navbar-text' }
78
})
8-
export class NavbarTextComponent {
9-
10-
@HostBinding('class.navbar-text') navbarTextClass = true;
11-
12-
}
9+
export class NavbarTextComponent {}
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
import { NavbarTogglerDirective } from './navbar-toggler.directive';
21
import { ElementRef, Renderer2 } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import { NavbarTogglerDirective } from './navbar-toggler.directive';
34

45
describe('NavbarTogglerDirective', () => {
5-
let renderer: Renderer2;
6-
let hostElement: ElementRef;
6+
beforeEach(() => {
7+
TestBed.configureTestingModule({
8+
providers: [Renderer2, { provide: ElementRef, useValue: { nativeElement: document.createElement('button') } }]
9+
});
10+
});
711

812
it('should create an instance', () => {
9-
const directive = new NavbarTogglerDirective(renderer, hostElement);
10-
expect(directive).toBeTruthy();
13+
TestBed.runInInjectionContext(() => {
14+
const directive = new NavbarTogglerDirective();
15+
expect(directive).toBeTruthy();
16+
});
1117
});
1218
});
Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,60 @@
1-
import { AfterContentInit, Directive, ElementRef, HostBinding, HostListener, Input, Renderer2 } from '@angular/core';
1+
import { afterNextRender, Directive, ElementRef, HostListener, inject, input, Renderer2 } from '@angular/core';
22
import { CollapseDirective } from '../../collapse';
33

44
@Directive({
55
selector: '[cNavbarToggler]',
6-
standalone: true
6+
standalone: true,
7+
host: {
8+
'[attr.aria-label]': 'ariaLabel()',
9+
'[attr.type]': 'type()',
10+
class: 'navbar-toggler'
11+
}
712
})
8-
export class NavbarTogglerDirective implements AfterContentInit {
13+
export class NavbarTogglerDirective {
14+
readonly #renderer = inject(Renderer2);
15+
readonly #hostElement = inject(ElementRef);
16+
17+
constructor() {
18+
afterNextRender({
19+
read: () => {
20+
const hasContent = this.#hostElement.nativeElement.childNodes.length as boolean;
21+
if (!hasContent) {
22+
this.addDefaultIcon();
23+
}
24+
}
25+
});
26+
}
27+
928
/**
1029
* Reference to navbar collapse element (via # template variable) . [docs]
1130
* @type string
1231
* @default 'button'
1332
*/
14-
@Input('cNavbarToggler') collapseRef?: CollapseDirective;
15-
@HostBinding('class.navbar-toggler') navbarToggler = true;
33+
readonly collapseRef = input<CollapseDirective | undefined>(undefined, { alias: 'cNavbarToggler' });
34+
1635
/**
1736
* Default type for navbar-toggler. [docs]
1837
* @type string
1938
* @default 'button'
2039
*/
21-
@HostBinding('attr.type')
22-
@Input() type = 'button';
40+
readonly type = input('button');
41+
2342
/**
2443
* Default aria-label attr for navbar-toggler. [docs]
2544
* @type string
2645
* @default 'Toggle navigation'
2746
*/
28-
@HostBinding('attr.aria-label')
29-
@Input() ariaLabel = 'Toggle navigation';
30-
31-
private hasContent!: boolean;
32-
33-
constructor(
34-
private renderer: Renderer2,
35-
private hostElement: ElementRef
36-
) { }
47+
readonly ariaLabel = input('Toggle navigation');
3748

3849
@HostListener('click', ['$event'])
3950
handleClick() {
40-
this.collapseRef?.toggle(!this.collapseRef?.visible);
51+
const collapseRef = this.collapseRef();
52+
collapseRef?.toggle(!collapseRef?.visible());
4153
}
4254

4355
addDefaultIcon(): void {
44-
const span = this.renderer.createElement('span');
45-
this.renderer.addClass(span, 'navbar-toggler-icon');
46-
this.renderer.appendChild(this.hostElement.nativeElement, span);
47-
}
48-
49-
ngAfterContentInit(): void {
50-
this.hasContent = this.hostElement.nativeElement.childNodes.length as boolean;
51-
if (!this.hasContent) {
52-
this.addDefaultIcon();
53-
}
56+
const span = this.#renderer.createElement('span');
57+
this.#renderer.addClass(span, 'navbar-toggler-icon');
58+
this.#renderer.appendChild(this.#hostElement.nativeElement, span);
5459
}
5560
}

‎projects/coreui-angular/src/lib/navbar/navbar.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<ng-container *ngTemplateOutlet="container ? withContainerTemplate : noContainerTemplate" />
1+
<ng-container *ngTemplateOutlet="container() ? withContainerTemplate : noContainerTemplate" />
22

33
<ng-template #withContainerTemplate>
4-
<div [ngClass]="containerClass">
4+
<div [ngClass]="containerClass()">
55
<ng-content />
66
</div>
77
</ng-template>
Lines changed: 87 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
import { AfterContentInit, Component, ContentChild, ElementRef, HostBinding, Input } from '@angular/core';
2-
import { NgClass, NgTemplateOutlet } from '@angular/common';
1+
import {
2+
AfterContentInit,
3+
afterRender,
4+
Component,
5+
computed,
6+
contentChild,
7+
ElementRef,
8+
inject,
9+
input,
10+
OnDestroy,
11+
signal
12+
} from '@angular/core';
13+
import { DOCUMENT, NgClass, NgTemplateOutlet } from '@angular/common';
314
import { BreakpointObserver } from '@angular/cdk/layout';
415

516
import { CollapseDirective } from '../collapse';
617
import { Colors } from '../coreui.types';
718
import { ThemeDirective } from '../shared';
19+
import { Subscription } from 'rxjs';
820

921
// todo: fix container prop issue not rendering children
1022
// todo: workaround - use <c-container> component directly in template
@@ -15,79 +27,106 @@ import { ThemeDirective } from '../shared';
1527
standalone: true,
1628
imports: [NgClass, NgTemplateOutlet],
1729
hostDirectives: [{ directive: ThemeDirective, inputs: ['colorScheme'] }],
18-
host: { class: 'navbar' }
30+
host: { '[class]': 'hostClasses()', '[attr.role]': 'role()' }
1931
})
20-
export class NavbarComponent implements AfterContentInit {
32+
export class NavbarComponent implements AfterContentInit, OnDestroy {
33+
readonly #breakpointObserver = inject(BreakpointObserver);
34+
readonly #document = inject(DOCUMENT);
35+
readonly #hostElement = inject(ElementRef);
36+
2137
/**
2238
* Sets the color context of the component to one of CoreUI’s themed colors.
2339
* @type Colors
2440
*/
25-
@Input() color?: Colors;
41+
readonly color = input<Colors>();
42+
2643
/**
2744
* Defines optional container wrapping children elements.
2845
*/
29-
@Input() container?: boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'fluid';
46+
readonly container = input<boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'fluid'>();
47+
3048
/**
3149
* Defines the responsive breakpoint to determine when content collapses.
3250
*/
33-
@Input() expand?: boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
51+
readonly expand = input<boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'>();
52+
3453
/**
3554
* Place component in non-static positions.
3655
*/
37-
@Input() placement?: 'fixed-top' | 'fixed-bottom' | 'sticky-top';
56+
readonly placement = input<'fixed-top' | 'fixed-bottom' | 'sticky-top'>();
3857

39-
@ContentChild(CollapseDirective) collapse!: CollapseDirective;
58+
readonly role = input('navigation');
4059

41-
@HostBinding('attr.role')
42-
@Input()
43-
role = 'navigation';
60+
readonly collapse = contentChild(CollapseDirective);
4461

45-
constructor(
46-
private hostElement: ElementRef,
47-
private breakpointObserver: BreakpointObserver
48-
) {}
49-
50-
@HostBinding('class')
51-
get hostClasses(): any {
52-
const expandClassSuffix: string = this.expand === true ? '' : `-${this.expand}`;
62+
readonly hostClasses = computed(() => {
63+
const color = this.color();
64+
const expand = this.expand();
65+
const expandClassSuffix: string = expand === true ? '' : `-${expand}`;
66+
const placement = this.placement();
5367
return {
5468
navbar: true,
55-
[`navbar-expand${expandClassSuffix}`]: !!this.expand,
56-
[`bg-${this.color}`]: !!this.color,
57-
[`${this.placement}`]: !!this.placement
58-
};
59-
}
69+
[`navbar-expand${expandClassSuffix}`]: !!expand,
70+
[`bg-${color}`]: !!color,
71+
[`${placement}`]: !!placement
72+
} as Record<string, boolean>;
73+
});
6074

61-
get containerClass(): string {
62-
return `container${this.container !== true ? '-' + this.container : ''}`;
63-
}
75+
readonly containerClass = computed(() => {
76+
const container = this.container();
77+
return `container${container !== true ? '-' + container : ''}`;
78+
});
6479

65-
get breakpoint(): string | boolean {
66-
if (typeof this.expand === 'string') {
67-
return (
68-
getComputedStyle(this.hostElement.nativeElement)?.getPropertyValue(`--cui-breakpoint-${this.expand}`) ?? false
69-
);
80+
readonly computedStyle = signal<string>('');
81+
82+
readonly afterNextRenderFn = afterRender({
83+
read: () => {
84+
const expand = this.expand();
85+
if (typeof expand === 'string') {
86+
const computedStyle =
87+
this.#document.defaultView
88+
?.getComputedStyle(this.#hostElement.nativeElement)
89+
?.getPropertyValue(`--cui-breakpoint-${expand}`) ?? false;
90+
computedStyle && this.computedStyle.set(computedStyle);
91+
}
92+
}
93+
});
94+
95+
readonly breakpoint = computed(() => {
96+
const expand = this.expand();
97+
if (typeof expand === 'string') {
98+
return this.computedStyle();
7099
}
71100
return false;
72-
}
101+
});
102+
103+
#observer!: Subscription;
73104

74105
ngAfterContentInit(): void {
75-
if (this.breakpoint) {
76-
const onBreakpoint = `(min-width: ${this.breakpoint})`;
77-
this.breakpointObserver.observe([onBreakpoint]).subscribe((result) => {
78-
if (this.collapse) {
79-
const animate = this.collapse.animate;
80-
// todo: collapse animate input signal setter
81-
this.collapse.animate = false;
82-
this.collapse.toggle(false);
83-
setTimeout(() => {
84-
this.collapse.toggle(result.matches);
106+
const breakpoint = this.breakpoint();
107+
if (breakpoint) {
108+
const onBreakpoint = `(min-width: ${breakpoint})`;
109+
this.#observer = this.#breakpointObserver
110+
.observe([onBreakpoint])
111+
.pipe()
112+
.subscribe((result) => {
113+
const collapse = this.collapse();
114+
if (collapse) {
115+
const animate = collapse.animate();
116+
collapse.animate.set(false);
117+
collapse.toggle(false);
85118
setTimeout(() => {
86-
this.collapse.animate = animate;
119+
collapse.toggle(result.matches);
120+
setTimeout(() => {
121+
collapse.animate.set(animate);
122+
});
87123
});
88-
});
89-
}
90-
});
124+
}
125+
});
91126
}
92127
}
128+
129+
ngOnDestroy(): void {
130+
this.#observer?.unsubscribe();
131+
}
93132
}

0 commit comments

Comments
 (0)
Please sign in to comment.