Skip to content

Commit e0c0050

Browse files
maxokorokovpkozlowski-opensource
authored andcommitted
refactor(carousel): re-implement with observables and clean up API
Fixes #1319 Closes #2494
1 parent ee3917b commit e0c0050

File tree

2 files changed

+59
-89
lines changed

2 files changed

+59
-89
lines changed

src/carousel/carousel.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('ngb-carousel', () => {
3636

3737
it('should initialize inputs with default values', () => {
3838
const defaultConfig = new NgbCarouselConfig();
39-
const carousel = new NgbCarousel(new NgbCarouselConfig());
39+
const carousel = new NgbCarousel(new NgbCarouselConfig(), null, null);
4040

4141
expect(carousel.interval).toBe(defaultConfig.interval);
4242
expect(carousel.wrap).toBe(defaultConfig.wrap);

src/carousel/carousel.ts

+58-88
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import {
2+
AfterContentChecked,
3+
AfterContentInit,
24
Component,
3-
Directive,
4-
TemplateRef,
55
ContentChildren,
6-
QueryList,
6+
Directive,
7+
EventEmitter,
8+
Inject,
79
Input,
8-
AfterContentChecked,
9-
OnInit,
10+
NgZone,
1011
OnChanges,
1112
OnDestroy,
1213
Output,
13-
EventEmitter
14+
PLATFORM_ID,
15+
QueryList,
16+
TemplateRef
1417
} from '@angular/core';
18+
import {isPlatformBrowser} from '@angular/common';
19+
1520
import {NgbCarouselConfig} from './carousel-config';
1621

22+
import {Subject, timer} from 'rxjs';
23+
import {filter, map, switchMap, takeUntil} from 'rxjs/operators';
24+
1725
let nextId = 0;
1826

1927
/**
@@ -39,35 +47,37 @@ export class NgbSlide {
3947
'class': 'carousel slide',
4048
'[style.display]': '"block"',
4149
'tabIndex': '0',
42-
'(mouseenter)': 'onMouseEnter()',
43-
'(mouseleave)': 'onMouseLeave()',
44-
'(keydown.arrowLeft)': 'keyPrev()',
45-
'(keydown.arrowRight)': 'keyNext()'
50+
'(mouseenter)': 'pauseOnHover && pause()',
51+
'(mouseleave)': 'pauseOnHover && cycle()',
52+
'(keydown.arrowLeft)': 'keyboard && prev()',
53+
'(keydown.arrowRight)': 'keyboard && next()'
4654
},
4755
template: `
4856
<ol class="carousel-indicators" *ngIf="showNavigationIndicators">
4957
<li *ngFor="let slide of slides" [id]="slide.id" [class.active]="slide.id === activeId"
50-
(click)="cycleToSelected(slide.id, getSlideEventDirection(activeId, slide.id))"></li>
58+
(click)="select(slide.id); pauseOnHover && pause()"></li>
5159
</ol>
5260
<div class="carousel-inner">
5361
<div *ngFor="let slide of slides" class="carousel-item" [class.active]="slide.id === activeId">
5462
<ng-template [ngTemplateOutlet]="slide.tplRef"></ng-template>
5563
</div>
5664
</div>
57-
<a class="carousel-control-prev" role="button" (click)="cycleToPrev()" *ngIf="showNavigationArrows">
65+
<a class="carousel-control-prev" role="button" (click)="prev()" *ngIf="showNavigationArrows">
5866
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
5967
<span class="sr-only" i18n="@@ngb.carousel.previous">Previous</span>
6068
</a>
61-
<a class="carousel-control-next" role="button" (click)="cycleToNext()" *ngIf="showNavigationArrows">
69+
<a class="carousel-control-next" role="button" (click)="next()" *ngIf="showNavigationArrows">
6270
<span class="carousel-control-next-icon" aria-hidden="true"></span>
6371
<span class="sr-only" i18n="@@ngb.carousel.next">Next</span>
6472
</a>
65-
`
73+
`
6674
})
6775
export class NgbCarousel implements AfterContentChecked,
68-
OnDestroy, OnInit, OnChanges {
76+
AfterContentInit, OnChanges, OnDestroy {
6977
@ContentChildren(NgbSlide) slides: QueryList<NgbSlide>;
70-
private _slideChangeInterval;
78+
79+
private _start$ = new Subject<void>();
80+
private _stop$ = new Subject<void>();
7181

7282
/**
7383
* The active slide id.
@@ -114,7 +124,7 @@ export class NgbCarousel implements AfterContentChecked,
114124
*/
115125
@Output() slide = new EventEmitter<NgbSlideEvent>();
116126

117-
constructor(config: NgbCarouselConfig) {
127+
constructor(config: NgbCarouselConfig, @Inject(PLATFORM_ID) private _platformId, private _ngZone: NgZone) {
118128
this.interval = config.interval;
119129
this.wrap = config.wrap;
120130
this.keyboard = config.keyboard;
@@ -123,117 +133,77 @@ export class NgbCarousel implements AfterContentChecked,
123133
this.showNavigationIndicators = config.showNavigationIndicators;
124134
}
125135

136+
ngAfterContentInit() {
137+
// setInterval() doesn't play well with SSR and protractor,
138+
// so we should run it in the browser and outside Angular
139+
if (isPlatformBrowser(this._platformId)) {
140+
this._ngZone.runOutsideAngular(() => {
141+
this._start$
142+
.pipe(
143+
map(() => this.interval), filter(interval => interval > 0),
144+
switchMap(interval => timer(interval).pipe(takeUntil(this._stop$))))
145+
.subscribe(() => this._ngZone.run(() => this.next()));
146+
147+
this._start$.next();
148+
});
149+
}
150+
}
151+
126152
ngAfterContentChecked() {
127153
let activeSlide = this._getSlideById(this.activeId);
128154
this.activeId = activeSlide ? activeSlide.id : (this.slides.length ? this.slides.first.id : null);
129155
}
130156

131-
ngOnInit() { this._startTimer(); }
157+
ngOnDestroy() { this._stop$.next(); }
132158

133159
ngOnChanges(changes) {
134160
if ('interval' in changes && !changes['interval'].isFirstChange()) {
135-
this._restartTimer();
161+
this._start$.next();
136162
}
137163
}
138164

139-
ngOnDestroy() { clearInterval(this._slideChangeInterval); }
140-
141165
/**
142166
* Navigate to a slide with the specified identifier.
143167
*/
144-
select(slideId: string) {
145-
this.cycleToSelected(slideId, this.getSlideEventDirection(this.activeId, slideId));
146-
this._restartTimer();
147-
}
168+
select(slideId: string) { this._cycleToSelected(slideId, this._getSlideEventDirection(this.activeId, slideId)); }
148169

149170
/**
150171
* Navigate to the next slide.
151172
*/
152-
prev() {
153-
this.cycleToPrev();
154-
this._restartTimer();
155-
}
173+
prev() { this._cycleToSelected(this._getPrevSlide(this.activeId), NgbSlideEventDirection.RIGHT); }
156174

157175
/**
158176
* Navigate to the next slide.
159177
*/
160-
next() {
161-
this.cycleToNext();
162-
this._restartTimer();
163-
}
178+
next() { this._cycleToSelected(this._getNextSlide(this.activeId), NgbSlideEventDirection.LEFT); }
164179

165180
/**
166181
* Stops the carousel from cycling through items.
167182
*/
168-
pause() { this._stopTimer(); }
183+
pause() { this._stop$.next(); }
169184

170185
/**
171186
* Restarts cycling through the carousel slides from left to right.
172187
*/
173-
cycle() { this._startTimer(); }
174-
175-
cycleToNext() { this.cycleToSelected(this._getNextSlide(this.activeId), NgbSlideEventDirection.LEFT); }
188+
cycle() { this._start$.next(); }
176189

177-
cycleToPrev() { this.cycleToSelected(this._getPrevSlide(this.activeId), NgbSlideEventDirection.RIGHT); }
178-
179-
cycleToSelected(slideIdx: string, direction: NgbSlideEventDirection) {
190+
private _cycleToSelected(slideIdx: string, direction: NgbSlideEventDirection) {
180191
let selectedSlide = this._getSlideById(slideIdx);
181-
if (selectedSlide) {
182-
if (selectedSlide.id !== this.activeId) {
183-
this.slide.emit({prev: this.activeId, current: selectedSlide.id, direction: direction});
184-
}
192+
if (selectedSlide && selectedSlide.id !== this.activeId) {
193+
this.slide.emit({prev: this.activeId, current: selectedSlide.id, direction: direction});
194+
this._start$.next();
185195
this.activeId = selectedSlide.id;
186196
}
187197
}
188198

189-
getSlideEventDirection(currentActiveSlideId: string, nextActiveSlideId: string): NgbSlideEventDirection {
199+
private _getSlideEventDirection(currentActiveSlideId: string, nextActiveSlideId: string): NgbSlideEventDirection {
190200
const currentActiveSlideIdx = this._getSlideIdxById(currentActiveSlideId);
191201
const nextActiveSlideIdx = this._getSlideIdxById(nextActiveSlideId);
192202

193203
return currentActiveSlideIdx > nextActiveSlideIdx ? NgbSlideEventDirection.RIGHT : NgbSlideEventDirection.LEFT;
194204
}
195205

196-
keyPrev() {
197-
if (this.keyboard) {
198-
this.prev();
199-
}
200-
}
201-
202-
keyNext() {
203-
if (this.keyboard) {
204-
this.next();
205-
}
206-
}
207-
208-
onMouseEnter() {
209-
if (this.pauseOnHover) {
210-
this.pause();
211-
}
212-
}
213-
214-
onMouseLeave() {
215-
if (this.pauseOnHover) {
216-
this.cycle();
217-
}
218-
}
219-
220-
private _restartTimer() {
221-
this._stopTimer();
222-
this._startTimer();
223-
}
224-
225-
private _startTimer() {
226-
if (this.interval > 0) {
227-
this._slideChangeInterval = setInterval(() => { this.cycleToNext(); }, this.interval);
228-
}
229-
}
230-
231-
private _stopTimer() { clearInterval(this._slideChangeInterval); }
232-
233-
private _getSlideById(slideId: string): NgbSlide {
234-
let slideWithId: NgbSlide[] = this.slides.filter(slide => slide.id === slideId);
235-
return slideWithId.length ? slideWithId[0] : null;
236-
}
206+
private _getSlideById(slideId: string): NgbSlide { return this.slides.find(slide => slide.id === slideId); }
237207

238208
private _getSlideIdxById(slideId: string): number {
239209
return this.slides.toArray().indexOf(this._getSlideById(slideId));
@@ -259,8 +229,8 @@ export class NgbCarousel implements AfterContentChecked,
259229
}
260230

261231
/**
262-
* The payload of the slide event fired when the slide transition is completed
263-
*/
232+
* The payload of the slide event fired when the slide transition is completed
233+
*/
264234
export interface NgbSlideEvent {
265235
/**
266236
* Previous slide id

0 commit comments

Comments
 (0)