Skip to content

Commit be23845

Browse files
committed
refactor(progress): add progress-bar props for simplified use with [value], add progress-stacked component, update testing, rewrite with signals
1 parent 5debc8a commit be23845

14 files changed

+401
-142
lines changed
Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,67 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
22

33
import { ProgressBarComponent } from './progress-bar.component';
4+
import { ProgressBarDirective } from './progress-bar.directive';
45

56
describe('ProgressBarComponent', () => {
67
let component: ProgressBarComponent;
78
let fixture: ComponentFixture<ProgressBarComponent>;
89

910
beforeEach(waitForAsync(() => {
1011
TestBed.configureTestingModule({
11-
imports: [ProgressBarComponent]
12-
})
13-
.compileComponents();
14-
}));
12+
imports: [ProgressBarComponent, ProgressBarDirective]
13+
}).compileComponents();
1514

16-
beforeEach(() => {
1715
fixture = TestBed.createComponent(ProgressBarComponent);
16+
1817
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;
1922
fixture.detectChanges();
20-
});
23+
}));
2124

2225
it('should create', () => {
23-
expect(component).toBeTruthy();
26+
expect(component).toBeDefined();
27+
});
28+
29+
it('should have css class="progress-bar"', () => {
30+
expect(fixture.nativeElement.classList.contains('progress-bar')).toBeTruthy();
31+
expect(fixture.nativeElement.classList.contains('bg-success')).toBeTruthy();
32+
expect(fixture.nativeElement.classList.contains('progress-bar-striped')).toBeTruthy();
33+
expect(fixture.nativeElement.classList.contains('progress-bar-animated')).toBeTruthy();
34+
});
35+
36+
it('should have style width %', () => {
37+
expect(fixture.nativeElement.style.width).toBe('42%');
38+
});
39+
40+
it('should have aria-* attributes', () => {
41+
expect(fixture.nativeElement.getAttribute('aria-valuenow')).toBe('42');
42+
expect(fixture.nativeElement.getAttribute('aria-valuemin')).toBe('0');
43+
expect(fixture.nativeElement.getAttribute('aria-valuemax')).toBe('100');
44+
expect(fixture.nativeElement.getAttribute('role')).toBe('progressbar');
45+
});
46+
47+
it('should not have aria-* attributes', () => {
48+
fixture.debugElement.injector.get(ProgressBarDirective).value = undefined;
49+
fixture.detectChanges();
50+
expect(fixture.nativeElement.getAttribute('aria-valuenow')).toBeFalsy();
51+
expect(fixture.nativeElement.getAttribute('aria-valuemin')).toBeFalsy();
52+
expect(fixture.nativeElement.getAttribute('aria-valuemax')).toBeFalsy();
53+
expect(fixture.nativeElement.getAttribute('role')).toBeFalsy();
54+
expect(fixture.nativeElement.style.width).toBeFalsy();
55+
});
56+
57+
it('should not have aria-* attributes', () => {
58+
fixture.debugElement.injector.get(ProgressBarDirective).value = undefined;
59+
fixture.debugElement.injector.get(ProgressBarDirective).width = 84;
60+
fixture.detectChanges();
61+
expect(fixture.nativeElement.getAttribute('aria-valuenow')).toBeFalsy();
62+
expect(fixture.nativeElement.getAttribute('aria-valuemin')).toBeFalsy();
63+
expect(fixture.nativeElement.getAttribute('aria-valuemax')).toBeFalsy();
64+
expect(fixture.nativeElement.getAttribute('role')).toBeFalsy();
65+
expect(fixture.nativeElement.style.width).toBe('84%');
2466
});
2567
});
Lines changed: 18 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,30 @@
1-
import {
2-
booleanAttribute,
3-
Component,
4-
ElementRef,
5-
HostBinding,
6-
Input,
7-
numberAttribute,
8-
OnChanges,
9-
OnInit,
10-
Renderer2,
11-
SimpleChanges
12-
} from '@angular/core';
13-
import { Colors } from '../coreui.types';
1+
import { ChangeDetectionStrategy, Component, HostBinding, inject } from '@angular/core';
2+
import { ProgressBarDirective } from './progress-bar.directive';
143

154
@Component({
165
selector: 'c-progress-bar',
17-
template: '<ng-content></ng-content>',
18-
standalone: true
6+
template: '<ng-content />',
7+
standalone: true,
8+
hostDirectives: [{
9+
directive: ProgressBarDirective,
10+
inputs: ['animated', 'color', 'max', 'role', 'stacked', 'value', 'variant', 'width']
11+
}],
12+
changeDetection: ChangeDetectionStrategy.OnPush
1913
})
20-
export class ProgressBarComponent implements OnInit, OnChanges {
14+
export class ProgressBarComponent {
2115

22-
/**
23-
* Use to animate the stripes right to left via CSS3 animations.
24-
* @type boolean
25-
*/
26-
@Input({ transform: booleanAttribute }) animated: string | boolean = false;
27-
28-
/**
29-
* Sets the color context of the component to one of CoreUI’s themed colors.
30-
* @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light'
31-
*/
32-
@Input() color?: Colors;
33-
// TODO: check if this is necessary.
34-
@Input({ transform: numberAttribute }) precision: string | number = 0;
35-
/**
36-
* The percent to progress the ProgressBar.
37-
* @type number
38-
*/
39-
@Input({ transform: numberAttribute }) value: string | number = 0;
40-
41-
/**
42-
* Set the progress bar variant to optional striped.
43-
* @values 'striped'
44-
*/
45-
@Input() variant?: 'striped';
46-
47-
/**
48-
* Set default html role attribute.
49-
* @type string
50-
*/
51-
@Input()
52-
@HostBinding('attr.role') role = 'progressbar';
53-
private state = {
54-
percent: 0,
55-
min: 0,
56-
max: 100
57-
};
58-
59-
constructor(
60-
private renderer: Renderer2,
61-
private hostElement: ElementRef
62-
) { }
63-
64-
get min(): number {
65-
return this.state.min;
66-
}
67-
68-
@Input()
69-
set min(value: number) {
70-
this.state.min = isNaN(value) ? 0 : value;
71-
}
72-
73-
get max(): number {
74-
return this.state.max;
75-
}
76-
77-
@Input()
78-
set max(value: number) {
79-
this.state.max = isNaN(value) || value <= 0 || value === this.min ? 100 : value;
80-
}
16+
readonly #progressBarDirective: ProgressBarDirective | null = inject(ProgressBarDirective, { optional: true });
8117

8218
@HostBinding('class')
83-
get hostClasses(): any {
19+
get hostClasses(): Record<string, boolean> {
20+
const animated = this.#progressBarDirective?.animated;
21+
const color = this.#progressBarDirective?.color;
22+
const variant = this.#progressBarDirective?.variant;
8423
return {
8524
'progress-bar': true,
86-
'progress-bar-animated': this.animated,
87-
[`progress-bar-${this.variant}`]: !!this.variant,
88-
[`bg-${this.color}`]: !!this.color
25+
'progress-bar-animated': !!animated,
26+
[`progress-bar-${variant}`]: !!variant,
27+
[`bg-${color}`]: !!color
8928
};
9029
}
91-
92-
ngOnInit(): void {
93-
this.setValues();
94-
}
95-
96-
setPercent(): void {
97-
this.state.percent = +((<number>this.value / (this.max - this.min)) * 100).toFixed(<number>this.precision);
98-
}
99-
100-
setValues(): void {
101-
this.setPercent();
102-
const host: HTMLElement = this.hostElement.nativeElement;
103-
this.renderer.setStyle(host, 'width', `${this.state.percent}%`);
104-
this.renderer.setAttribute(host, 'aria-valuenow', String(this.value));
105-
this.renderer.setAttribute(host, 'aria-valuemin', String(this.min));
106-
this.renderer.setAttribute(host, 'aria-valuemax', String(this.max));
107-
}
108-
109-
public ngOnChanges(changes: SimpleChanges): void {
110-
this.setValues();
111-
}
11230
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ElementRef, Renderer2 } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import { ProgressBarDirective } from './progress-bar.directive';
4+
5+
class MockElementRef extends ElementRef {}
6+
7+
describe('ProgressBarDirective', () => {
8+
let directive: ProgressBarDirective;
9+
10+
beforeEach(() => {
11+
12+
TestBed.configureTestingModule({
13+
providers: [
14+
Renderer2,
15+
{ provide: ElementRef, useClass: MockElementRef }
16+
]
17+
});
18+
19+
TestBed.runInInjectionContext(() => {
20+
directive = new ProgressBarDirective();
21+
});
22+
});
23+
24+
it('should create an instance', () => {
25+
expect(directive).toBeDefined();
26+
});
27+
28+
it('should have percent value', () => {
29+
directive.value = 42;
30+
expect(directive.percent()).toBe(42);
31+
});
32+
33+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {
2+
booleanAttribute,
3+
computed,
4+
Directive,
5+
effect,
6+
EffectRef,
7+
ElementRef,
8+
inject,
9+
Input,
10+
numberAttribute,
11+
Renderer2,
12+
signal,
13+
WritableSignal
14+
} from '@angular/core';
15+
import { Colors } from '../coreui.types';
16+
import { IProgressBar } from './progress.type';
17+
18+
@Directive({
19+
selector: '[cProgressBar]',
20+
standalone: true
21+
})
22+
export class ProgressBarDirective implements IProgressBar {
23+
24+
constructor() {}
25+
26+
readonly #renderer = inject(Renderer2);
27+
readonly #hostElement = inject(ElementRef);
28+
29+
readonly #max = signal(100);
30+
readonly #min = 0;
31+
readonly #value: WritableSignal<number | undefined> = signal(undefined);
32+
readonly #width: WritableSignal<number | undefined> = signal(undefined);
33+
34+
readonly percent = computed(() => {
35+
return +((((this.#value() ?? this.#width() ?? 0) - this.#min) / (this.#max() - this.#min)) * 100).toFixed(this.precision);
36+
});
37+
38+
readonly #valuesEffect: EffectRef = effect(() => {
39+
const host: HTMLElement = this.#hostElement.nativeElement;
40+
if (this.#value() === undefined || this.#width()) {
41+
for (let name of ['aria-valuenow', 'aria-valuemax', 'aria-valuemin', 'role']) {
42+
this.#renderer.removeAttribute(host, name);
43+
}
44+
} else {
45+
this.#renderer.setAttribute(host, 'aria-valuenow', String(this.#value()));
46+
this.#renderer.setAttribute(host, 'aria-valuemin', String(this.#min));
47+
this.#renderer.setAttribute(host, 'aria-valuemax', String(this.#max()));
48+
this.#renderer.setAttribute(host, 'role', this.role);
49+
}
50+
const tagName = host.tagName;
51+
if (this.percent() && ((this.stacked && tagName === 'C-PROGRESS') || (!this.stacked && tagName !== 'C-PROGRESS'))) {
52+
this.#renderer.setStyle(host, 'width', `${this.percent()}%`);
53+
} else {
54+
this.#renderer.removeStyle(host, 'width');
55+
}
56+
});
57+
58+
/**
59+
* Use to animate the stripes right to left via CSS3 animations.
60+
* @type boolean
61+
*/
62+
@Input({ transform: booleanAttribute }) animated?: boolean;
63+
64+
/**
65+
* Sets the color context of the component to one of CoreUI’s themed colors.
66+
* @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light'
67+
*/
68+
@Input() color?: Colors;
69+
70+
// TODO: check if this is necessary.
71+
@Input({ transform: numberAttribute }) precision: number = 0;
72+
73+
/**
74+
* The percent value the ProgressBar.
75+
* @type number
76+
* @default 0
77+
*/
78+
@Input({ transform: numberAttribute })
79+
set value(value: number | undefined) {
80+
this.#value.set(value);
81+
}
82+
83+
get value() {
84+
return this.#value();
85+
}
86+
87+
@Input({ transform: numberAttribute })
88+
set width(value: number | undefined) {
89+
this.#width.set(value);
90+
}
91+
92+
/**
93+
* Set the progress bar variant to optional striped.
94+
* @values 'striped'
95+
* @default undefined
96+
*/
97+
@Input() variant?: 'striped';
98+
99+
/**
100+
* The max value of the ProgressBar.
101+
* @type number
102+
* @default 100
103+
*/
104+
@Input({ transform: numberAttribute })
105+
set max(max: number) {
106+
this.#max.set(isNaN(max) || max <= 0 ? 100 : max);
107+
}
108+
109+
/**
110+
* Stacked ProgressBars.
111+
* @type boolean
112+
* @default false
113+
*/
114+
@Input({ transform: booleanAttribute }) stacked?: boolean = false;
115+
116+
/**
117+
* Set default html role attribute.
118+
* @type string
119+
*/
120+
@Input() role: string = 'progressbar';
121+
122+
}

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

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { ProgressStackedComponent } from './progress-stacked.component';
4+
5+
describe('ProgressStackedComponent', () => {
6+
let component: ProgressStackedComponent;
7+
let fixture: ComponentFixture<ProgressStackedComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [ProgressStackedComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(ProgressStackedComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeDefined();
22+
expect(component.stacked).toBeTruthy();
23+
});
24+
});

0 commit comments

Comments
 (0)