Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: replace setTimeout with timer to make it cancellable #431

Merged
merged 1 commit into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/ngu/carousel/src/lib/ngu-carousel.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class NguCarouselPointDirective {}
selector: '[nguCarouselDef]'
})
export class NguCarouselDefDirective<T> {
when: (index: number, nodeData: T) => boolean;
when?: (index: number, nodeData: T) => boolean;
santoshyadavdev marked this conversation as resolved.
Show resolved Hide resolved

constructor(public template: TemplateRef<any>) {}
}
Expand Down
174 changes: 92 additions & 82 deletions libs/ngu/carousel/src/lib/ngu-carousel/ngu-carousel.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { isPlatformBrowser } from '@angular/common';

import {
AfterContentInit,
AfterViewInit,
Expand All @@ -22,12 +20,10 @@ import {
OnDestroy,
OnInit,
Output,
PLATFORM_ID,
QueryList,
Renderer2,
TrackByFunction,
ViewChild,
ViewContainerRef
ViewChild
} from '@angular/core';
import {
EMPTY,
Expand All @@ -42,12 +38,14 @@ import {
timer
} from 'rxjs';
import { debounceTime, filter, map, startWith, switchMap, takeUntil } from 'rxjs/operators';

import {
NguCarouselDefDirective,
NguCarouselNextDirective,
NguCarouselOutlet,
NguCarouselPrevDirective
} from './../ngu-carousel.directive';
import { IS_BROWSER } from '../symbols';
import {
Transfrom,
Breakpoints,
Expand Down Expand Up @@ -83,12 +81,7 @@ export class NguCarousel<T>
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
@Output() onMove = new EventEmitter<NguCarousel<T>>();
// isFirstss = 0;
arrayChanges: IterableChanges<{}>;
carouselInt: Subscription;

listener1: () => void;
listener2: () => void;
listener3: () => void;
private _arrayChanges: IterableChanges<{}> | null = null;

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

@ContentChildren(NguCarouselDefDirective)
private _defDirec: QueryList<NguCarouselDefDirective<any>>;
private _defDirectives: QueryList<NguCarouselDefDirective<any>>;

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

/** The setter is used to catch the button if the button has ngIf
* issue id #91
/**
* The setter is used to catch the button if the button is wrapped with `ngIf`.
* https://github.com/uiuniversal/ngu-carousel/issues/91
*/
@ContentChild(NguCarouselNextDirective, /* TODO: add static flag */ { read: ElementRef })
set nextBtn(btn: ElementRef) {
this.listener2?.();
if (btn) {
this.listener2 = this._renderer.listen(btn.nativeElement, 'click', () =>
this._carouselScrollOne(1)
);
}
@ContentChild(NguCarouselNextDirective, { read: ElementRef, static: false })
set nextButton(nextButton: ElementRef<HTMLElement> | undefined) {
this._nextButton$.next(nextButton?.nativeElement);
}

/** The setter is used to catch the button if the button has ngIf
* issue id #91
/**
* The setter is used to catch the button if the button is wrapped with `ngIf`.
* https://github.com/uiuniversal/ngu-carousel/issues/91
*/
@ContentChild(NguCarouselPrevDirective, /* TODO: add static flag */ { read: ElementRef })
set prevBtn(btn: ElementRef) {
this.listener1?.();
if (btn) {
this.listener1 = this._renderer.listen(btn.nativeElement, 'click', () =>
this._carouselScrollOne(0)
);
}
@ContentChild(NguCarouselPrevDirective, { read: ElementRef, static: false })
set prevButton(prevButton: ElementRef<HTMLElement> | undefined) {
this._prevButton$.next(prevButton?.nativeElement);
}

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

@ViewChild('touchContainer', { read: ElementRef, static: true })
private touchContainer: ElementRef;
private _touchContainer: ElementRef<HTMLElement>;

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

Expand Down Expand Up @@ -171,16 +156,21 @@ export class NguCarousel<T>
}
private _trackByFn: TrackByFunction<T>;

/** Subjects used to notify whenever buttons are removed or rendered so we can re-add listeners. */
private readonly _prevButton$ = new Subject<HTMLElement | undefined>();
private readonly _nextButton$ = new Subject<HTMLElement | undefined>();

constructor(
private _el: ElementRef,
private _renderer: Renderer2,
private _differs: IterableDiffers,
@Inject(PLATFORM_ID) private platformId: object,
@Inject(IS_BROWSER) private _isBrowser: boolean,
private _cdr: ChangeDetectorRef,
private _ngZone: NgZone,
private _nguWindowScrollListener: NguWindowScrollListener
) {
super();
this._setupButtonListeners();
}

ngOnInit() {
Expand All @@ -190,15 +180,15 @@ export class NguCarousel<T>
}

ngDoCheck() {
this.arrayChanges = this._dataDiffer.diff(this.dataSource)!;
if (this.arrayChanges && this._defDirec) {
this._arrayChanges = this._dataDiffer.diff(this.dataSource)!;
if (this._arrayChanges && this._defDirectives) {
this._observeRenderChanges();
}
}

private _switchDataSource(dataSource: any): any {
this._dataSource = dataSource;
if (this._defDirec) {
if (this._defDirectives) {
this._observeRenderChanges();
}
}
Expand All @@ -222,13 +212,12 @@ export class NguCarousel<T>
}
}

private renderNodeChanges(
data: any[],
viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer
) {
if (!this.arrayChanges) return;
private renderNodeChanges(data: any[]) {
if (!this._arrayChanges) return;

this.arrayChanges.forEachOperation(
const viewContainer = this._nodeOutlet.viewContainer;

this._arrayChanges.forEachOperation(
(
item: IterableChangeRecord<any>,
adjustedPreviousIndex: number | null,
Expand Down Expand Up @@ -274,12 +263,12 @@ export class NguCarousel<T>
}

private _getNodeDef(data: any, i: number): NguCarouselDefDirective<any> {
if (this._defDirec.length === 1) {
return this._defDirec.first;
if (this._defDirectives.length === 1) {
return this._defDirectives.first;
}

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

return nodeDef;
}
Expand All @@ -290,7 +279,7 @@ export class NguCarousel<T>

this.carouselCssNode = this._createStyleElem();

if (isPlatformBrowser(this.platformId)) {
if (this._isBrowser) {
this._carouselInterval();
if (!this.vertical.enabled && this.inputs.touch) {
this._setupHammer();
Expand Down Expand Up @@ -338,17 +327,8 @@ export class NguCarousel<T>
ngOnDestroy() {
this._hammertime?.destroy();
this._destroy$.next();
this.carouselInt && this.carouselInt.unsubscribe();
this._intervalController$.unsubscribe();
this.carouselLoad.complete();
this.onMove.complete();

/** remove listeners */
for (let i = 1; i <= 3; i++) {
// TODO: revisit later.
const str = `listener${i}` as 'listener1' | 'listener2' | 'listener3';
this[str] && this[str]();
}
}

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

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

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

Expand Down Expand Up @@ -831,20 +811,31 @@ export class NguCarousel<T>

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

setTimeout(() => {
this.carouselInt = merge(play$, touchPlay$, pause$, touchPause$, this._intervalController$)
.pipe(
startWith(1),
switchMap(val => {
this.isHovered = !val;
this._cdr.markForCheck();
return val ? interval$ : EMPTY;
})
)
.subscribe(() => {
this._carouselScrollOne(1);
});
}, this.interval.initialDelay);
const initialDelay = this.interval.initialDelay || 0;

const carouselInterval$ = merge(
play$,
touchPlay$,
pause$,
touchPause$,
this._intervalController$
).pipe(
startWith(1),
switchMap(val => {
this.isHovered = !val;
this._cdr.markForCheck();
return val ? interval$ : EMPTY;
})
);

timer(initialDelay)
.pipe(
switchMap(() => carouselInterval$),
takeUntil(this._destroy$)
)
.subscribe(() => {
this._carouselScrollOne(1);
});
}
}

Expand All @@ -854,39 +845,41 @@ export class NguCarousel<T>
start: number,
end: number,
speed: number,
length: number,
viewContainer = this._nodeOutlet.viewContainer
length: number
): void {
const viewContainer = this._nodeOutlet.viewContainer;

let val = length < 5 ? length : 5;
val = val === 1 ? 3 : val;
const collectIndex: number[] = [];
const collectedIndexes: number[] = [];

if (direction === 1) {
for (let i = start - 1; i < end; i++) {
collectIndex.push(i);
collectedIndexes.push(i);
val = val * 2;
const viewRef = viewContainer.get(i) as any;
const context = viewRef.context as any;
context.animate = { value: true, params: { distance: val } };
}
} else {
for (let i = end - 1; i >= start - 1; i--) {
collectIndex.push(i);
collectedIndexes.push(i);
val = val * 2;
const viewRef = viewContainer.get(i) as any;
const context = viewRef.context as any;
context.animate = { value: true, params: { distance: -val } };
}
}
this._cdr.markForCheck();
setTimeout(() => {
this._removeAnimations(collectIndex);
}, speed * 0.7);

timer(speed * 0.7)
.pipe(takeUntil(this._destroy$))
.subscribe(() => this._removeAnimations(collectedIndexes));
}

private _removeAnimations(indexs: number[]) {
private _removeAnimations(collectedIndexes: number[]) {
const viewContainer = this._nodeOutlet.viewContainer;
indexs.forEach(i => {
collectedIndexes.forEach(i => {
const viewRef = viewContainer.get(i) as any;
const context = viewRef.context as any;
context.animate = { value: false, params: { distance: 0 } };
Expand All @@ -910,6 +903,23 @@ export class NguCarousel<T>
return styleItem;
}

private _setupButtonListeners(): void {
this._prevButton$
.pipe(
// Returning `EMPTY` will remove event listener once the button is removed from the DOM.
switchMap(prevButton => (prevButton ? fromEvent(prevButton, 'click') : EMPTY)),
takeUntil(this._destroy$)
)
.subscribe(() => this._carouselScrollOne(0));

this._nextButton$
.pipe(
switchMap(nextButton => (nextButton ? fromEvent(nextButton, 'click') : EMPTY)),
takeUntil(this._destroy$)
)
.subscribe(() => this._carouselScrollOne(1));
}

private _setupWindowResizeListener(): void {
this._ngZone.runOutsideAngular(() =>
fromEvent(window, 'resize')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, NgZone, OnDestroy, PLATFORM_ID } from '@angular/core';
import { Inject, Injectable, NgZone, OnDestroy } from '@angular/core';
import { Subject, fromEvent } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { IS_BROWSER } from '../symbols';

@Injectable({ providedIn: 'root' })
export class NguWindowScrollListener extends Subject<Event> implements OnDestroy {
private readonly _destroy$ = new Subject<void>();

constructor(@Inject(PLATFORM_ID) platformId: string, ngZone: NgZone) {
constructor(@Inject(IS_BROWSER) isBrowser: boolean, ngZone: NgZone) {
super();

// Note: this service is shared between multiple `NguCarousel` components and each instance
// doesn't add new events listener for the `window`.
if (isPlatformBrowser(platformId)) {
if (isBrowser) {
ngZone.runOutsideAngular(() =>
fromEvent(window, 'scroll').pipe(takeUntil(this._destroy$)).subscribe(this)
);
Expand Down
7 changes: 7 additions & 0 deletions libs/ngu/carousel/src/lib/symbols.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { isPlatformBrowser } from '@angular/common';
import { InjectionToken, PLATFORM_ID, inject } from '@angular/core';

export const IS_BROWSER = new InjectionToken<boolean>('IS_BROWSER', {
providedIn: 'root',
factory: () => isPlatformBrowser(inject(PLATFORM_ID))
});