Skip to content

Commit ae1aa1c

Browse files
committed
refactor(progress): signal inputs, host bindings, cleanup, tests, service introduction
1 parent 480611d commit ae1aa1c

10 files changed

+225
-160
lines changed

projects/coreui-angular/src/lib/progress/progress-bar.component.spec.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
22

3+
import { ComponentRef } from '@angular/core';
34
import { ProgressBarComponent } from './progress-bar.component';
45
import { ProgressBarDirective } from './progress-bar.directive';
6+
import { ProgressService } from './progress.service';
57

68
describe('ProgressBarComponent', () => {
79
let component: ProgressBarComponent;
10+
let componentRef: ComponentRef<ProgressBarComponent>;
811
let fixture: ComponentFixture<ProgressBarComponent>;
12+
let directive: ProgressBarDirective;
913

1014
beforeEach(waitForAsync(() => {
1115
TestBed.configureTestingModule({
12-
imports: [ProgressBarComponent, ProgressBarDirective]
16+
imports: [ProgressBarComponent],
17+
providers: [ProgressService]
1318
}).compileComponents();
1419

1520
fixture = TestBed.createComponent(ProgressBarComponent);
1621

1722
component = fixture.componentInstance;
18-
fixture.debugElement.injector.get(ProgressBarDirective).value = 42;
19-
fixture.debugElement.injector.get(ProgressBarDirective).color = 'success';
20-
fixture.debugElement.injector.get(ProgressBarDirective).variant = 'striped';
21-
fixture.debugElement.injector.get(ProgressBarDirective).animated = true;
23+
componentRef = fixture.componentRef;
24+
directive = fixture.debugElement.injector.get(ProgressBarDirective);
25+
componentRef.setInput('value', 42);
26+
componentRef.setInput('color', 'success');
27+
componentRef.setInput('variant', 'striped');
28+
componentRef.setInput('animated', true);
2229
fixture.detectChanges();
2330
}));
2431

@@ -45,7 +52,7 @@ describe('ProgressBarComponent', () => {
4552
});
4653

4754
it('should not have aria-* attributes', () => {
48-
fixture.debugElement.injector.get(ProgressBarDirective).value = undefined;
55+
componentRef.setInput('value', undefined);
4956
fixture.detectChanges();
5057
expect(fixture.nativeElement.getAttribute('aria-valuenow')).toBeFalsy();
5158
expect(fixture.nativeElement.getAttribute('aria-valuemin')).toBeFalsy();
@@ -54,15 +61,4 @@ describe('ProgressBarComponent', () => {
5461
// expect(fixture.nativeElement.style.width).toBeFalsy();
5562
expect(fixture.nativeElement.style.width).toBe('0%');
5663
});
57-
58-
it('should not have aria-* attributes', () => {
59-
fixture.debugElement.injector.get(ProgressBarDirective).value = undefined;
60-
fixture.debugElement.injector.get(ProgressBarDirective).width = 84;
61-
fixture.detectChanges();
62-
expect(fixture.nativeElement.getAttribute('aria-valuenow')).toBeFalsy();
63-
expect(fixture.nativeElement.getAttribute('aria-valuemin')).toBeFalsy();
64-
expect(fixture.nativeElement.getAttribute('aria-valuemax')).toBeFalsy();
65-
expect(fixture.nativeElement.getAttribute('role')).toBeFalsy();
66-
expect(fixture.nativeElement.style.width).toBe('84%');
67-
});
6864
});
Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, Component, HostBinding, inject } from '@angular/core';
1+
import { Component, computed, inject } from '@angular/core';
22
import { ProgressBarDirective } from './progress-bar.directive';
33

44
@Component({
@@ -7,25 +7,23 @@ import { ProgressBarDirective } from './progress-bar.directive';
77
hostDirectives: [
88
{
99
directive: ProgressBarDirective,
10-
inputs: ['animated', 'color', 'max', 'role', 'stacked', 'value', 'variant', 'width']
10+
inputs: ['animated', 'color', 'max', 'role', 'value', 'variant']
1111
}
1212
],
13-
changeDetection: ChangeDetectionStrategy.OnPush,
14-
host: { class: 'progress-bar' }
13+
host: { class: 'progress-bar', '[class]': 'hostClasses()' }
1514
})
1615
export class ProgressBarComponent {
1716
readonly #progressBarDirective: ProgressBarDirective | null = inject(ProgressBarDirective, { optional: true });
1817

19-
@HostBinding('class')
20-
get hostClasses(): Record<string, boolean> {
21-
const animated = this.#progressBarDirective?.animated;
22-
const color = this.#progressBarDirective?.color;
23-
const variant = this.#progressBarDirective?.variant;
18+
readonly hostClasses = computed(() => {
19+
const animated = this.#progressBarDirective?.animated();
20+
const color = this.#progressBarDirective?.color();
21+
const variant = this.#progressBarDirective?.variant();
2422
return {
2523
'progress-bar': true,
2624
'progress-bar-animated': !!animated,
2725
[`progress-bar-${variant}`]: !!variant,
2826
[`bg-${color}`]: !!color
29-
};
30-
}
27+
} as Record<string, boolean>;
28+
});
3129
}
Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,81 @@
1-
import { ElementRef, Renderer2 } from '@angular/core';
2-
import { TestBed } from '@angular/core/testing';
1+
import { Component, ComponentRef, DebugElement, ElementRef, input, Renderer2 } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
33
import { ProgressBarDirective } from './progress-bar.directive';
4+
import { By } from '@angular/platform-browser';
5+
import { ProgressService } from './progress.service';
46

57
class MockElementRef extends ElementRef {}
68

9+
@Component({
10+
template: `<div cProgressBar [value]="value()" [color]="color()" variant="striped" animated></div>`,
11+
selector: 'c-test',
12+
imports: [ProgressBarDirective]
13+
})
14+
export class TestComponent {
15+
readonly value = input(42);
16+
readonly color = input('success');
17+
}
18+
719
describe('ProgressBarDirective', () => {
820
let directive: ProgressBarDirective;
21+
let debugElement: DebugElement;
22+
let fixture: ComponentFixture<TestComponent>;
23+
let componentRef: ComponentRef<TestComponent>;
924

1025
beforeEach(() => {
11-
1226
TestBed.configureTestingModule({
13-
providers: [
14-
Renderer2,
15-
{ provide: ElementRef, useClass: MockElementRef }
16-
]
27+
providers: [Renderer2, { provide: ElementRef, useClass: MockElementRef }, ProgressService],
28+
imports: [TestComponent]
1729
});
30+
fixture = TestBed.createComponent(TestComponent);
31+
componentRef = fixture.componentRef;
32+
debugElement = fixture.debugElement.query(By.directive(ProgressBarDirective));
33+
directive = debugElement.injector.get(ProgressBarDirective);
34+
fixture.detectChanges();
1835

19-
TestBed.runInInjectionContext(() => {
20-
directive = new ProgressBarDirective();
21-
});
36+
// TestBed.runInInjectionContext(() => {
37+
// directive = new ProgressBarDirective();
38+
// });
2239
});
2340

2441
it('should create an instance', () => {
2542
expect(directive).toBeDefined();
2643
});
2744

28-
it('should have percent value', () => {
29-
directive.value = 42;
30-
expect(directive.percent()).toBe(42);
45+
it('should have color value', () => {
46+
expect(directive.color()).toBe('success');
47+
});
48+
49+
it('should have max value', () => {
50+
expect(directive.max()).toBe(100);
51+
});
52+
53+
it('should have variant value', () => {
54+
expect(directive.variant()).toBe('striped');
3155
});
3256

57+
it('should have role value', () => {
58+
expect(directive.role()).toBe('progressbar');
59+
});
60+
61+
it('should have precision value', () => {
62+
expect(directive.precision()).toBe(0);
63+
});
64+
65+
it('should have animated value', () => {
66+
expect(directive.animated()).toBe(true);
67+
});
68+
69+
it('should have aria-* attributes', () => {
70+
expect(debugElement.nativeElement.getAttribute('aria-valuenow')).toBe('42');
71+
expect(debugElement.nativeElement.getAttribute('aria-valuemin')).toBe('0');
72+
expect(debugElement.nativeElement.getAttribute('aria-valuemax')).toBe('100');
73+
expect(debugElement.nativeElement.getAttribute('role')).toBe('progressbar');
74+
componentRef.setInput('value', undefined);
75+
fixture.detectChanges();
76+
expect(debugElement.nativeElement.getAttribute('aria-valuenow')).toBeNull();
77+
expect(debugElement.nativeElement.getAttribute('aria-valuemin')).toBeNull();
78+
expect(debugElement.nativeElement.getAttribute('aria-valuemax')).toBeNull();
79+
expect(debugElement.nativeElement.getAttribute('role')).toBeNull();
80+
});
3381
});
Lines changed: 37 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,96 @@
11
import {
22
booleanAttribute,
3-
computed,
43
Directive,
54
effect,
65
EffectRef,
76
ElementRef,
87
inject,
9-
Input,
8+
input,
109
numberAttribute,
11-
Renderer2,
12-
signal,
13-
WritableSignal
10+
Renderer2
1411
} from '@angular/core';
1512
import { Colors } from '../coreui.types';
16-
import { IProgressBar } from './progress.type';
13+
import { ProgressService } from './progress.service';
1714

1815
@Directive({
19-
selector: '[cProgressBar]'
16+
selector: '[cProgressBar]',
17+
exportAs: 'cProgressBar'
2018
})
21-
export class ProgressBarDirective implements IProgressBar {
19+
export class ProgressBarDirective {
2220
readonly #renderer = inject(Renderer2);
2321
readonly #hostElement = inject(ElementRef);
24-
25-
readonly #max = signal(100);
26-
readonly #min = 0;
27-
readonly #value: WritableSignal<number | undefined> = signal(undefined);
28-
readonly #width: WritableSignal<number | undefined> = signal(undefined);
29-
30-
readonly percent = computed(() => {
31-
return +((((this.#value() ?? this.#width() ?? 0) - this.#min) / (this.#max() - this.#min)) * 100).toFixed(
32-
this.precision
33-
);
34-
});
22+
readonly #progressService = inject(ProgressService);
3523

3624
readonly #valuesEffect: EffectRef = effect(() => {
3725
const host: HTMLElement = this.#hostElement.nativeElement;
38-
if (this.#value() === undefined || this.#width()) {
26+
const value = this.#progressService.value();
27+
const percent = this.#progressService.percent();
28+
const stacked = this.#progressService.stacked();
29+
if (value === undefined) {
3930
for (const name of ['aria-valuenow', 'aria-valuemax', 'aria-valuemin', 'role']) {
4031
this.#renderer.removeAttribute(host, name);
4132
}
4233
} else {
43-
this.#renderer.setAttribute(host, 'aria-valuenow', String(this.#value()));
44-
this.#renderer.setAttribute(host, 'aria-valuemin', String(this.#min));
45-
this.#renderer.setAttribute(host, 'aria-valuemax', String(this.#max()));
46-
this.#renderer.setAttribute(host, 'role', this.role);
34+
const { min, max } = this.#progressService;
35+
this.#renderer.setAttribute(host, 'aria-valuenow', String(value));
36+
this.#renderer.setAttribute(host, 'aria-valuemin', String(min()));
37+
this.#renderer.setAttribute(host, 'aria-valuemax', String(max()));
38+
this.#renderer.setAttribute(host, 'role', this.role());
4739
}
4840
const tagName = host.tagName;
49-
if (
50-
this.percent() >= 0 &&
51-
((this.stacked && tagName === 'C-PROGRESS') || (!this.stacked && tagName !== 'C-PROGRESS'))
52-
) {
53-
this.#renderer.setStyle(host, 'width', `${this.percent()}%`);
41+
if (percent >= 0 && ((stacked && tagName === 'C-PROGRESS') || (!stacked && tagName !== 'C-PROGRESS'))) {
42+
this.#renderer.setStyle(host, 'width', `${percent}%`);
5443
} else {
5544
this.#renderer.removeStyle(host, 'width');
5645
}
5746
});
5847

5948
/**
6049
* Use to animate the stripes right to left via CSS3 animations.
61-
* @type boolean
50+
* @return boolean
6251
*/
63-
@Input({ transform: booleanAttribute }) animated?: boolean;
52+
readonly animated = input<boolean, unknown>(undefined, { transform: booleanAttribute });
6453

6554
/**
6655
* Sets the color context of the component to one of CoreUI’s themed colors.
6756
* @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light'
6857
*/
69-
@Input() color?: Colors;
58+
readonly color = input<Colors>();
7059

71-
// TODO: check if this is necessary.
72-
@Input({ transform: numberAttribute }) precision: number = 0;
60+
readonly precision = input(0, { transform: numberAttribute });
7361

7462
/**
7563
* The percent value the ProgressBar.
76-
* @type number
64+
* @return number
7765
* @default 0
7866
*/
79-
@Input({ transform: numberAttribute })
80-
set value(value: number | undefined) {
81-
this.#value.set(value);
82-
}
83-
84-
get value() {
85-
return this.#value();
86-
}
87-
88-
@Input({ transform: numberAttribute })
89-
set width(value: number | undefined) {
90-
this.#width.set(value);
91-
}
67+
readonly value = input(undefined, { transform: numberAttribute });
9268

9369
/**
9470
* Set the progress bar variant to optional striped.
9571
* @values 'striped'
9672
* @default undefined
9773
*/
98-
@Input() variant?: 'striped';
74+
readonly variant = input<'striped'>();
9975

10076
/**
10177
* The max value of the ProgressBar.
102-
* @type number
78+
* @return number
10379
* @default 100
10480
*/
105-
@Input({ transform: numberAttribute })
106-
set max(max: number) {
107-
this.#max.set(isNaN(max) || max <= 0 ? 100 : max);
108-
}
109-
110-
/**
111-
* Stacked ProgressBars.
112-
* @type boolean
113-
* @default false
114-
*/
115-
@Input({ transform: booleanAttribute }) stacked?: boolean = false;
81+
readonly max = input(100, { transform: numberAttribute });
11682

11783
/**
11884
* Set default html role attribute.
119-
* @type string
85+
* @return string
12086
*/
121-
@Input() role: string = 'progressbar';
87+
readonly role = input<string>('progressbar');
88+
89+
readonly #serviceEffect = effect(() => {
90+
this.#progressService.precision.set(this.precision());
91+
const max = this.max();
92+
this.#progressService.max.set(isNaN(max) || max <= 0 ? 100 : max);
93+
const value = this.value();
94+
this.#progressService.value.set(value && !isNaN(value) ? value : undefined);
95+
});
12296
}
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core';
2-
import { IProgressBarStacked } from './progress.type';
1+
import { Component, input } from '@angular/core';
32

43
@Component({
54
selector: 'c-progress-stacked',
5+
exportAs: 'cProgressStacked',
66
template: '<ng-content />',
77
styles: `
88
:host {
99
display: flex;
1010
}
1111
`,
12-
changeDetection: ChangeDetectionStrategy.OnPush
12+
host: { '[class.progress-stacked]': 'stacked()' }
1313
})
14-
export class ProgressStackedComponent implements IProgressBarStacked {
15-
@Input()
16-
@HostBinding('class.progress-stacked')
17-
stacked = true;
14+
export class ProgressStackedComponent {
15+
readonly stacked = input(true);
1816
}

projects/coreui-angular/src/lib/progress/progress.component.html

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
@if (contentProgressBars.length) {
1+
@if (contentProgressBars()?.length) {
22
<ng-container *ngTemplateOutlet="defaultContent" />
3-
} @else if (pbd?.stacked) {
4-
<c-progress-bar [animated]="pbd?.animated" [variant]="pbd?.variant" [color]="pbd?.color" stacked>
5-
<ng-container *ngTemplateOutlet="defaultContent" />
6-
</c-progress-bar>
73
} @else {
8-
<c-progress-bar [width]="pbd?.percent()" [animated]="pbd?.animated" [variant]="pbd?.variant" [color]="pbd?.color">
4+
@let pbd = progressBarDirective;
5+
<c-progress-bar [animated]="pbd?.animated()" [variant]="pbd?.variant()" [color]="pbd?.color()" [value]="value()">
96
<ng-container *ngTemplateOutlet="defaultContent" />
107
</c-progress-bar>
118
}

0 commit comments

Comments
 (0)