Skip to content

Commit 0f44e98

Browse files
authored
fix: replace setTimeout with timer to make it cancellable (#431)
1 parent 69e46a0 commit 0f44e98

File tree

4 files changed

+105
-87
lines changed

4 files changed

+105
-87
lines changed

libs/ngu/carousel/src/lib/ngu-carousel.directive.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class NguCarouselPointDirective {}
3434
selector: '[nguCarouselDef]'
3535
})
3636
export class NguCarouselDefDirective<T> {
37-
when: (index: number, nodeData: T) => boolean;
37+
when?: (index: number, nodeData: T) => boolean;
3838

3939
constructor(public template: TemplateRef<any>) {}
4040
}

libs/ngu/carousel/src/lib/ngu-carousel/ngu-carousel.component.ts

Lines changed: 92 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { isPlatformBrowser } from '@angular/common';
2-
31
import {
42
AfterContentInit,
53
AfterViewInit,
@@ -22,12 +20,10 @@ import {
2220
OnDestroy,
2321
OnInit,
2422
Output,
25-
PLATFORM_ID,
2623
QueryList,
2724
Renderer2,
2825
TrackByFunction,
29-
ViewChild,
30-
ViewContainerRef
26+
ViewChild
3127
} from '@angular/core';
3228
import {
3329
EMPTY,
@@ -42,12 +38,14 @@ import {
4238
timer
4339
} from 'rxjs';
4440
import { debounceTime, filter, map, startWith, switchMap, takeUntil } from 'rxjs/operators';
41+
4542
import {
4643
NguCarouselDefDirective,
4744
NguCarouselNextDirective,
4845
NguCarouselOutlet,
4946
NguCarouselPrevDirective
5047
} from './../ngu-carousel.directive';
48+
import { IS_BROWSER } from '../symbols';
5149
import {
5250
Transfrom,
5351
Breakpoints,
@@ -83,12 +81,7 @@ export class NguCarousel<T>
8381
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
8482
@Output() onMove = new EventEmitter<NguCarousel<T>>();
8583
// isFirstss = 0;
86-
arrayChanges: IterableChanges<{}>;
87-
carouselInt: Subscription;
88-
89-
listener1: () => void;
90-
listener2: () => void;
91-
listener3: () => void;
84+
private _arrayChanges: IterableChanges<{}> | null = null;
9285

9386
@Input('dataSource')
9487
get dataSource(): any {
@@ -103,35 +96,27 @@ export class NguCarousel<T>
10396
private _defaultNodeDef: NguCarouselDefDirective<any> | null;
10497

10598
@ContentChildren(NguCarouselDefDirective)
106-
private _defDirec: QueryList<NguCarouselDefDirective<any>>;
99+
private _defDirectives: QueryList<NguCarouselDefDirective<any>>;
107100

108101
@ViewChild(NguCarouselOutlet, { static: true })
109102
_nodeOutlet: NguCarouselOutlet;
110103

111-
/** The setter is used to catch the button if the button has ngIf
112-
* issue id #91
104+
/**
105+
* The setter is used to catch the button if the button is wrapped with `ngIf`.
106+
* https://github.com/uiuniversal/ngu-carousel/issues/91
113107
*/
114-
@ContentChild(NguCarouselNextDirective, /* TODO: add static flag */ { read: ElementRef })
115-
set nextBtn(btn: ElementRef) {
116-
this.listener2?.();
117-
if (btn) {
118-
this.listener2 = this._renderer.listen(btn.nativeElement, 'click', () =>
119-
this._carouselScrollOne(1)
120-
);
121-
}
108+
@ContentChild(NguCarouselNextDirective, { read: ElementRef, static: false })
109+
set nextButton(nextButton: ElementRef<HTMLElement> | undefined) {
110+
this._nextButton$.next(nextButton?.nativeElement);
122111
}
123112

124-
/** The setter is used to catch the button if the button has ngIf
125-
* issue id #91
113+
/**
114+
* The setter is used to catch the button if the button is wrapped with `ngIf`.
115+
* https://github.com/uiuniversal/ngu-carousel/issues/91
126116
*/
127-
@ContentChild(NguCarouselPrevDirective, /* TODO: add static flag */ { read: ElementRef })
128-
set prevBtn(btn: ElementRef) {
129-
this.listener1?.();
130-
if (btn) {
131-
this.listener1 = this._renderer.listen(btn.nativeElement, 'click', () =>
132-
this._carouselScrollOne(0)
133-
);
134-
}
117+
@ContentChild(NguCarouselPrevDirective, { read: ElementRef, static: false })
118+
set prevButton(prevButton: ElementRef<HTMLElement> | undefined) {
119+
this._prevButton$.next(prevButton?.nativeElement);
135120
}
136121

137122
@ViewChild('ngucarousel', { read: ElementRef, static: true })
@@ -141,7 +126,7 @@ export class NguCarousel<T>
141126
private nguItemsContainer: ElementRef;
142127

143128
@ViewChild('touchContainer', { read: ElementRef, static: true })
144-
private touchContainer: ElementRef;
129+
private _touchContainer: ElementRef<HTMLElement>;
145130

146131
private _intervalController$ = new Subject<number>();
147132

@@ -171,16 +156,21 @@ export class NguCarousel<T>
171156
}
172157
private _trackByFn: TrackByFunction<T>;
173158

159+
/** Subjects used to notify whenever buttons are removed or rendered so we can re-add listeners. */
160+
private readonly _prevButton$ = new Subject<HTMLElement | undefined>();
161+
private readonly _nextButton$ = new Subject<HTMLElement | undefined>();
162+
174163
constructor(
175164
private _el: ElementRef,
176165
private _renderer: Renderer2,
177166
private _differs: IterableDiffers,
178-
@Inject(PLATFORM_ID) private platformId: object,
167+
@Inject(IS_BROWSER) private _isBrowser: boolean,
179168
private _cdr: ChangeDetectorRef,
180169
private _ngZone: NgZone,
181170
private _nguWindowScrollListener: NguWindowScrollListener
182171
) {
183172
super();
173+
this._setupButtonListeners();
184174
}
185175

186176
ngOnInit() {
@@ -190,15 +180,15 @@ export class NguCarousel<T>
190180
}
191181

192182
ngDoCheck() {
193-
this.arrayChanges = this._dataDiffer.diff(this.dataSource)!;
194-
if (this.arrayChanges && this._defDirec) {
183+
this._arrayChanges = this._dataDiffer.diff(this.dataSource)!;
184+
if (this._arrayChanges && this._defDirectives) {
195185
this._observeRenderChanges();
196186
}
197187
}
198188

199189
private _switchDataSource(dataSource: any): any {
200190
this._dataSource = dataSource;
201-
if (this._defDirec) {
191+
if (this._defDirectives) {
202192
this._observeRenderChanges();
203193
}
204194
}
@@ -222,13 +212,12 @@ export class NguCarousel<T>
222212
}
223213
}
224214

225-
private renderNodeChanges(
226-
data: any[],
227-
viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer
228-
) {
229-
if (!this.arrayChanges) return;
215+
private renderNodeChanges(data: any[]) {
216+
if (!this._arrayChanges) return;
230217

231-
this.arrayChanges.forEachOperation(
218+
const viewContainer = this._nodeOutlet.viewContainer;
219+
220+
this._arrayChanges.forEachOperation(
232221
(
233222
item: IterableChangeRecord<any>,
234223
adjustedPreviousIndex: number | null,
@@ -274,12 +263,12 @@ export class NguCarousel<T>
274263
}
275264

276265
private _getNodeDef(data: any, i: number): NguCarouselDefDirective<any> {
277-
if (this._defDirec.length === 1) {
278-
return this._defDirec.first;
266+
if (this._defDirectives.length === 1) {
267+
return this._defDirectives.first;
279268
}
280269

281270
const nodeDef: NguCarouselDefDirective<any> =
282-
this._defDirec.find(def => def.when && def.when(i, data)) || this._defaultNodeDef!;
271+
this._defDirectives.find(def => !!def.when?.(i, data)) || this._defaultNodeDef!;
283272

284273
return nodeDef;
285274
}
@@ -290,7 +279,7 @@ export class NguCarousel<T>
290279

291280
this.carouselCssNode = this._createStyleElem();
292281

293-
if (isPlatformBrowser(this.platformId)) {
282+
if (this._isBrowser) {
294283
this._carouselInterval();
295284
if (!this.vertical.enabled && this.inputs.touch) {
296285
this._setupHammer();
@@ -338,17 +327,8 @@ export class NguCarousel<T>
338327
ngOnDestroy() {
339328
this._hammertime?.destroy();
340329
this._destroy$.next();
341-
this.carouselInt && this.carouselInt.unsubscribe();
342-
this._intervalController$.unsubscribe();
343330
this.carouselLoad.complete();
344331
this.onMove.complete();
345-
346-
/** remove listeners */
347-
for (let i = 1; i <= 3; i++) {
348-
// TODO: revisit later.
349-
const str = `listener${i}` as 'listener1' | 'listener2' | 'listener3';
350-
this[str] && this[str]();
351-
}
352332
}
353333

354334
/** Get Touch input */
@@ -359,7 +339,7 @@ export class NguCarousel<T>
359339
// the HammerJS is loaded.
360340
.pipe(takeUntil(this._destroy$))
361341
.subscribe(() => {
362-
const hammertime = (this._hammertime = new Hammer(this.touchContainer.nativeElement));
342+
const hammertime = (this._hammertime = new Hammer(this._touchContainer.nativeElement));
363343
hammertime.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });
364344

365345
hammertime.on('panstart', (ev: any) => {
@@ -471,7 +451,7 @@ export class NguCarousel<T>
471451
/** store data based on width of the screen for the carousel */
472452
private _storeCarouselData(): void {
473453
const breakpoints = this.inputs.gridBreakpoints;
474-
this.deviceWidth = isPlatformBrowser(this.platformId) ? window.innerWidth : breakpoints?.xl!;
454+
this.deviceWidth = this._isBrowser ? window.innerWidth : breakpoints?.xl!;
475455

476456
this.carouselWidth = this.carouselMain1.nativeElement.offsetWidth;
477457

@@ -831,20 +811,31 @@ export class NguCarousel<T>
831811

832812
const interval$ = interval(this.inputs.interval?.timing!).pipe(mapToOne);
833813

834-
setTimeout(() => {
835-
this.carouselInt = merge(play$, touchPlay$, pause$, touchPause$, this._intervalController$)
836-
.pipe(
837-
startWith(1),
838-
switchMap(val => {
839-
this.isHovered = !val;
840-
this._cdr.markForCheck();
841-
return val ? interval$ : EMPTY;
842-
})
843-
)
844-
.subscribe(() => {
845-
this._carouselScrollOne(1);
846-
});
847-
}, this.interval.initialDelay);
814+
const initialDelay = this.interval.initialDelay || 0;
815+
816+
const carouselInterval$ = merge(
817+
play$,
818+
touchPlay$,
819+
pause$,
820+
touchPause$,
821+
this._intervalController$
822+
).pipe(
823+
startWith(1),
824+
switchMap(val => {
825+
this.isHovered = !val;
826+
this._cdr.markForCheck();
827+
return val ? interval$ : EMPTY;
828+
})
829+
);
830+
831+
timer(initialDelay)
832+
.pipe(
833+
switchMap(() => carouselInterval$),
834+
takeUntil(this._destroy$)
835+
)
836+
.subscribe(() => {
837+
this._carouselScrollOne(1);
838+
});
848839
}
849840
}
850841

@@ -854,39 +845,41 @@ export class NguCarousel<T>
854845
start: number,
855846
end: number,
856847
speed: number,
857-
length: number,
858-
viewContainer = this._nodeOutlet.viewContainer
848+
length: number
859849
): void {
850+
const viewContainer = this._nodeOutlet.viewContainer;
851+
860852
let val = length < 5 ? length : 5;
861853
val = val === 1 ? 3 : val;
862-
const collectIndex: number[] = [];
854+
const collectedIndexes: number[] = [];
863855

864856
if (direction === 1) {
865857
for (let i = start - 1; i < end; i++) {
866-
collectIndex.push(i);
858+
collectedIndexes.push(i);
867859
val = val * 2;
868860
const viewRef = viewContainer.get(i) as any;
869861
const context = viewRef.context as any;
870862
context.animate = { value: true, params: { distance: val } };
871863
}
872864
} else {
873865
for (let i = end - 1; i >= start - 1; i--) {
874-
collectIndex.push(i);
866+
collectedIndexes.push(i);
875867
val = val * 2;
876868
const viewRef = viewContainer.get(i) as any;
877869
const context = viewRef.context as any;
878870
context.animate = { value: true, params: { distance: -val } };
879871
}
880872
}
881873
this._cdr.markForCheck();
882-
setTimeout(() => {
883-
this._removeAnimations(collectIndex);
884-
}, speed * 0.7);
874+
875+
timer(speed * 0.7)
876+
.pipe(takeUntil(this._destroy$))
877+
.subscribe(() => this._removeAnimations(collectedIndexes));
885878
}
886879

887-
private _removeAnimations(indexs: number[]) {
880+
private _removeAnimations(collectedIndexes: number[]) {
888881
const viewContainer = this._nodeOutlet.viewContainer;
889-
indexs.forEach(i => {
882+
collectedIndexes.forEach(i => {
890883
const viewRef = viewContainer.get(i) as any;
891884
const context = viewRef.context as any;
892885
context.animate = { value: false, params: { distance: 0 } };
@@ -910,6 +903,23 @@ export class NguCarousel<T>
910903
return styleItem;
911904
}
912905

906+
private _setupButtonListeners(): void {
907+
this._prevButton$
908+
.pipe(
909+
// Returning `EMPTY` will remove event listener once the button is removed from the DOM.
910+
switchMap(prevButton => (prevButton ? fromEvent(prevButton, 'click') : EMPTY)),
911+
takeUntil(this._destroy$)
912+
)
913+
.subscribe(() => this._carouselScrollOne(0));
914+
915+
this._nextButton$
916+
.pipe(
917+
switchMap(nextButton => (nextButton ? fromEvent(nextButton, 'click') : EMPTY)),
918+
takeUntil(this._destroy$)
919+
)
920+
.subscribe(() => this._carouselScrollOne(1));
921+
}
922+
913923
private _setupWindowResizeListener(): void {
914924
this._ngZone.runOutsideAngular(() =>
915925
fromEvent(window, 'resize')

libs/ngu/carousel/src/lib/ngu-carousel/ngu-window-scroll-listener.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { isPlatformBrowser } from '@angular/common';
2-
import { Inject, Injectable, NgZone, OnDestroy, PLATFORM_ID } from '@angular/core';
1+
import { Inject, Injectable, NgZone, OnDestroy } from '@angular/core';
32
import { Subject, fromEvent } from 'rxjs';
43
import { takeUntil } from 'rxjs/operators';
54

5+
import { IS_BROWSER } from '../symbols';
6+
67
@Injectable({ providedIn: 'root' })
78
export class NguWindowScrollListener extends Subject<Event> implements OnDestroy {
89
private readonly _destroy$ = new Subject<void>();
910

10-
constructor(@Inject(PLATFORM_ID) platformId: string, ngZone: NgZone) {
11+
constructor(@Inject(IS_BROWSER) isBrowser: boolean, ngZone: NgZone) {
1112
super();
1213

1314
// Note: this service is shared between multiple `NguCarousel` components and each instance
1415
// doesn't add new events listener for the `window`.
15-
if (isPlatformBrowser(platformId)) {
16+
if (isBrowser) {
1617
ngZone.runOutsideAngular(() =>
1718
fromEvent(window, 'scroll').pipe(takeUntil(this._destroy$)).subscribe(this)
1819
);

libs/ngu/carousel/src/lib/symbols.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { isPlatformBrowser } from '@angular/common';
2+
import { InjectionToken, PLATFORM_ID, inject } from '@angular/core';
3+
4+
export const IS_BROWSER = new InjectionToken<boolean>('IS_BROWSER', {
5+
providedIn: 'root',
6+
factory: () => isPlatformBrowser(inject(PLATFORM_ID))
7+
});

0 commit comments

Comments
 (0)