Skip to content

Commit f61b4f3

Browse files
committed
refactor(carousel): signal inputs, host bindings, cleanup, tests
1 parent c141c5f commit f61b4f3

15 files changed

+311
-257
lines changed
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Component, HostBinding } from '@angular/core';
1+
import { Component } from '@angular/core';
22

33
@Component({
44
selector: 'c-carousel-caption',
55
template: '<ng-content />',
6-
styleUrls: ['./carousel-caption.component.scss']
6+
styleUrls: ['./carousel-caption.component.scss'],
7+
host: {
8+
'[class.carousel-caption]': 'true'
9+
}
710
})
8-
export class CarouselCaptionComponent {
9-
@HostBinding('class.carousel-caption') carouselCaptionClass = true;
10-
}
11+
export class CarouselCaptionComponent {}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
@if (hasContent) {
1+
@if (hasContent()) {
22
<div #content>
33
<ng-content />
44
</div>
55
} @else {
6-
<span [class]="carouselControlIconClass" [attr.aria-label]="direction" [attr.aria-hidden]="true"></span>
7-
<span class="visually-hidden">{{ caption }}</span>
6+
<span [class]="carouselControlIconClass()" [attr.aria-label]="direction()" [attr.aria-hidden]="true"></span>
7+
<span class="visually-hidden">{{ caption() }}</span>
88
}
Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
1-
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
1+
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
22

33
import { CarouselControlComponent } from './carousel-control.component';
44
import { CarouselService } from '../carousel.service';
55
import { CarouselState } from '../carousel-state';
6+
import { ComponentRef, DebugElement } from '@angular/core';
7+
import { take } from 'rxjs/operators';
68

79
describe('CarouselControlComponent', () => {
810
let component: CarouselControlComponent;
11+
let componentRef: ComponentRef<CarouselControlComponent>;
912
let fixture: ComponentFixture<CarouselControlComponent>;
1013
let service: CarouselService;
1114
let state: CarouselState;
15+
let debugElement: DebugElement;
1216

1317
beforeEach(waitForAsync(() => {
1418
TestBed.configureTestingModule({
1519
imports: [CarouselControlComponent],
1620
providers: [CarouselService, CarouselState]
17-
})
18-
.compileComponents();
21+
}).compileComponents();
1922
}));
2023

2124
beforeEach(() => {
2225
fixture = TestBed.createComponent(CarouselControlComponent);
2326
component = fixture.componentInstance;
27+
componentRef = fixture.componentRef;
2428
service = TestBed.inject(CarouselService);
2529
state = TestBed.inject(CarouselState);
30+
debugElement = fixture.debugElement;
2631
fixture.detectChanges();
2732
});
2833

@@ -33,4 +38,49 @@ describe('CarouselControlComponent', () => {
3338
it('should have css class="carousel-control-next"', () => {
3439
expect(fixture.nativeElement).toHaveClass('carousel-control-next');
3540
});
41+
42+
it('should have role="button"', () => {
43+
expect(fixture.nativeElement.getAttribute('role')).toBe('button');
44+
});
45+
46+
it('should have caption="Next"', () => {
47+
expect(component.caption()).toBe('Next');
48+
});
49+
50+
it('should have caption to be undefined', () => {
51+
componentRef.setInput('caption', 'Test');
52+
expect(component.caption()).toBe('Test');
53+
});
54+
55+
it('should have direction="next"', () => {
56+
expect(component.direction()).toBe('next');
57+
});
58+
59+
it('should have carouselControlIconClass="carousel-control-next-icon"', () => {
60+
expect(component.carouselControlIconClass()).toBe('carousel-control-next-icon');
61+
});
62+
63+
it('should play on click', fakeAsync(() => {
64+
componentRef.setInput('direction', 'prev');
65+
component.onClick(new MouseEvent('click'));
66+
fixture.detectChanges();
67+
expect(component.caption()).toBe('Previous');
68+
}));
69+
70+
it('should play on keyup', fakeAsync(() => {
71+
service.carouselIndex$.pipe(take(2)).subscribe((index) => {
72+
if (index.active === 0) {
73+
expect(index).toEqual({ active: 0, interval: -1, lastItemIndex: -1 });
74+
} else {
75+
expect(index).toEqual({});
76+
}
77+
});
78+
79+
debugElement.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));
80+
tick();
81+
debugElement.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight' }));
82+
tick();
83+
debugElement.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
84+
tick();
85+
}));
3686
});

projects/coreui-angular/src/lib/carousel/carousel-control/carousel-control.component.ts

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,77 @@
1-
import {
2-
AfterViewInit,
3-
ChangeDetectorRef,
4-
Component,
5-
ElementRef,
6-
HostBinding,
7-
HostListener,
8-
inject,
9-
Input,
10-
ViewChild
11-
} from '@angular/core';
1+
import { Component, computed, ElementRef, inject, input, linkedSignal, viewChild } from '@angular/core';
122

133
import { CarouselState } from '../carousel-state';
144

155
@Component({
166
selector: 'c-carousel-control',
17-
templateUrl: './carousel-control.component.html'
7+
templateUrl: './carousel-control.component.html',
8+
exportAs: 'cCarouselControl',
9+
host: {
10+
'[attr.role]': 'role()',
11+
'[class]': 'hostClasses()',
12+
'(keyup)': 'onKeyUp($event)',
13+
'(click)': 'onClick($event)'
14+
}
1815
})
19-
export class CarouselControlComponent implements AfterViewInit {
20-
readonly #changeDetectorRef = inject(ChangeDetectorRef);
16+
export class CarouselControlComponent {
2117
readonly #carouselState = inject(CarouselState);
2218

2319
/**
2420
* Carousel control caption. [docs]
25-
* @type string
21+
* @return string
2622
*/
27-
@Input()
28-
set caption(value) {
29-
this.#caption = value;
30-
}
23+
readonly captionInput = input<string | undefined>(undefined, { alias: 'caption' });
3124

32-
get caption(): string {
33-
return !!this.#caption ? this.#caption : this.direction === 'prev' ? 'Previous' : 'Next';
34-
}
35-
#caption?: string;
25+
readonly caption = linkedSignal({
26+
source: () => this.captionInput(),
27+
computation: (value) => {
28+
return !!value ? value : this.direction() === 'prev' ? 'Previous' : 'Next';
29+
}
30+
});
3631

3732
/**
38-
* Carousel control direction. [docs]
39-
* @type {'next' | 'prev'}
33+
* Carousel control direction.
34+
* @return {'next' | 'prev'}
4035
*/
41-
@Input() direction: 'prev' | 'next' = 'next';
36+
readonly direction = input<'prev' | 'next'>('next');
4237

43-
@HostBinding('attr.role')
44-
get hostRole(): string {
45-
return 'button';
46-
}
38+
/**
39+
* Carousel control role.
40+
* @return string
41+
*/
42+
readonly role = input('button');
4743

48-
@HostBinding('class')
49-
get hostClasses(): string {
50-
return `carousel-control-${this.direction}`;
51-
}
44+
readonly hostClasses = computed(() => {
45+
return `carousel-control-${this.direction()}`;
46+
});
5247

53-
get carouselControlIconClass(): string {
54-
return `carousel-control-${this.direction}-icon`;
55-
}
48+
readonly carouselControlIconClass = computed(() => {
49+
return `carousel-control-${this.direction()}-icon`;
50+
});
5651

57-
@ViewChild('content') content?: ElementRef;
52+
readonly content = viewChild('content', { read: ElementRef });
5853

59-
hasContent = true;
54+
readonly hasContent = computed(() => {
55+
return this.content()?.nativeElement.childNodes.length ?? false;
56+
});
6057

61-
@HostListener('keyup', ['$event'])
6258
onKeyUp($event: KeyboardEvent): void {
6359
if ($event.key === 'Enter') {
64-
this.play();
60+
this.#play();
6561
}
6662
if ($event.key === 'ArrowLeft') {
67-
this.play('prev');
63+
this.#play('prev');
6864
}
6965
if ($event.key === 'ArrowRight') {
70-
this.play('next');
66+
this.#play('next');
7167
}
7268
}
7369

74-
@HostListener('click', ['$event'])
75-
public onClick($event: MouseEvent): void {
76-
this.play();
77-
}
78-
79-
ngAfterViewInit(): void {
80-
this.hasContent = this.content?.nativeElement.childNodes.length ?? false;
81-
this.#changeDetectorRef.detectChanges();
70+
onClick($event: MouseEvent): void {
71+
this.#play();
8272
}
8373

84-
private play(direction = this.direction): void {
74+
#play(direction = this.direction()): void {
8575
const nextIndex = this.#carouselState.direction(direction);
8676
this.#carouselState.state = { activeItemIndex: nextIndex };
8777
}

projects/coreui-angular/src/lib/carousel/carousel-indicators/carousel-indicators.component.spec.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,30 @@ describe('CarouselIndicatorsComponent', () => {
1414
TestBed.configureTestingModule({
1515
imports: [CarouselIndicatorsComponent],
1616
providers: [CarouselService, CarouselState]
17-
})
18-
.compileComponents();
17+
}).compileComponents();
1918
}));
2019

2120
beforeEach(() => {
2221
fixture = TestBed.createComponent(CarouselIndicatorsComponent);
2322
service = TestBed.inject(CarouselService);
2423
state = TestBed.inject(CarouselState);
24+
state.setItems([]);
2525
component = fixture.componentInstance;
26+
component.items = [0, 1, 2, 3];
2627
fixture.detectChanges();
2728
});
2829

2930
it('should create', () => {
3031
expect(component).toBeTruthy();
3132
});
33+
34+
it('should set active index', () => {
35+
service.setIndex({ active: 1 });
36+
expect(component.active).toBe(1);
37+
});
38+
39+
it('should call onClick', () => {
40+
component.onClick(2);
41+
expect(component.active).toBe(2);
42+
});
3243
});
Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
2-
import { Subscription } from 'rxjs';
1+
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
32

43
import { CarouselState } from '../carousel-state';
54
import { CarouselService } from '../carousel.service';
5+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
66

77
@Component({
88
selector: 'c-carousel-indicators',
99
templateUrl: './carousel-indicators.component.html'
1010
})
11-
export class CarouselIndicatorsComponent implements OnInit, OnDestroy {
11+
export class CarouselIndicatorsComponent implements OnInit {
12+
readonly #destroyRef = inject(DestroyRef);
1213
readonly #carouselService = inject(CarouselService);
1314
readonly #carouselState = inject(CarouselState);
1415

1516
items: (number | undefined)[] = [];
1617
active = 0;
17-
#carouselIndexSubscription?: Subscription;
1818

1919
ngOnInit(): void {
20-
this.carouselStateSubscribe();
21-
}
22-
23-
ngOnDestroy(): void {
24-
this.carouselStateSubscribe(false);
20+
this.#carouselService.carouselIndex$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((nextIndex) => {
21+
this.items = this.#carouselState?.state?.items?.map((item) => item.index) ?? [];
22+
if ('active' in nextIndex) {
23+
this.active = nextIndex.active ?? 0;
24+
}
25+
});
2526
}
2627

2728
onClick(index: number): void {
@@ -30,17 +31,4 @@ export class CarouselIndicatorsComponent implements OnInit, OnDestroy {
3031
this.#carouselState.state = { direction, activeItemIndex: index };
3132
}
3233
}
33-
34-
private carouselStateSubscribe(subscribe: boolean = true): void {
35-
if (subscribe) {
36-
this.#carouselIndexSubscription = this.#carouselService.carouselIndex$.subscribe((nextIndex) => {
37-
this.items = this.#carouselState?.state?.items?.map((item) => item.index) ?? [];
38-
if ('active' in nextIndex) {
39-
this.active = nextIndex.active ?? 0;
40-
}
41-
});
42-
} else {
43-
this.#carouselIndexSubscription?.unsubscribe();
44-
}
45-
}
4634
}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
<div [@slideAnimation]="slide" [@.disabled]="!animate">
1+
<div [@slideAnimation]="slide()" [@.disabled]="!animate()">
22
<ng-content />
33
</div>
4-
<!--todo-->
5-
<!--<div [@fadeAnimation]="slide" [@.disabled]="!animate" >-->
6-
<!-- <ng-content />-->
7-
<!--</div>-->

0 commit comments

Comments
 (0)