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: set up Hammer outside of Angular to reduce CDs #443

Merged
merged 1 commit into from
Apr 18, 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
santoshyadavdev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { Observable, Subject, defer, fromEvent, map, shareReplay, takeUntil } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class NguHammerLoader {
private _hammer$ = defer(() => import('hammerjs')).pipe(
shareReplay({ bufferSize: 1, refCount: true })
);

load() {
return this._hammer$;
}
}

@Injectable()
export class NguCarouselHammerManager implements OnDestroy {
private _destroy$ = new Subject<void>();

constructor(private _ngZone: NgZone, private _nguHammerLoader: NguHammerLoader) {}

ngOnDestroy(): void {
this._destroy$.next();
}

createHammer(element: HTMLElement): Observable<HammerManager> {
return this._nguHammerLoader.load().pipe(
map(() =>
// Note: The Hammer manager should be created outside of the Angular zone since it sets up
// `pointermove` event listener which triggers change detection every time the pointer is moved.
this._ngZone.runOutsideAngular(() => new Hammer(element))
),
// Note: the dynamic import is always a microtask which may run after the view is destroyed.
// `takeUntil` is used to prevent setting Hammer up if the view had been destroyed before
// the HammerJS is loaded.
takeUntil(this._destroy$)
);
}

on(hammer: HammerManager, event: string) {
return fromEvent(hammer, event).pipe(
// Note: We have to re-enter the Angular zone because Hammer would trigger events outside of the
// Angular zone (since we set it up with `runOutsideAngular`).
enterNgZone(this._ngZone),
takeUntil(this._destroy$)
);
}
}

function enterNgZone<T>(ngZone: NgZone) {
return (source: Observable<T>) =>
new Observable<T>(subscriber =>
source.subscribe({
next: value => ngZone.run(() => subscriber.next(value)),
error: error => ngZone.run(() => subscriber.error(error)),
complete: () => ngZone.run(() => subscriber.complete())
})
);
}
64 changes: 30 additions & 34 deletions libs/ngu/carousel/src/lib/ngu-carousel/ngu-carousel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,7 @@ import {
TrackByFunction,
ViewChild
} from '@angular/core';
import {
EMPTY,
from,
fromEvent,
interval,
merge,
Observable,
of,
Subject,
Subscription,
timer
} from 'rxjs';
import { EMPTY, fromEvent, interval, merge, Observable, of, Subject, timer } from 'rxjs';
import { debounceTime, filter, map, startWith, switchMap, takeUntil } from 'rxjs/operators';

import {
Expand All @@ -53,6 +42,7 @@ import {
NguCarouselStore
} from './ngu-carousel';
import { NguWindowScrollListener } from './ngu-window-scroll-listener';
import { NguCarouselHammerManager } from './ngu-carousel-hammer-manager';

type DirectionSymbol = '' | '-';

Expand All @@ -68,7 +58,8 @@ const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode;
selector: 'ngu-carousel',
templateUrl: 'ngu-carousel.component.html',
styleUrls: ['ngu-carousel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NguCarouselHammerManager]
})
// eslint-disable-next-line @angular-eslint/component-class-suffix
export class NguCarousel<T>
Expand Down Expand Up @@ -146,7 +137,7 @@ export class NguCarousel<T>

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

private _hammertime: HammerManager | null = null;
private _hammer: HammerManager | null = null;

private _withAnimation = true;

Expand Down Expand Up @@ -191,7 +182,8 @@ export class NguCarousel<T>
@Inject(IS_BROWSER) private _isBrowser: boolean,
private _cdr: ChangeDetectorRef,
private _ngZone: NgZone,
private _nguWindowScrollListener: NguWindowScrollListener
private _nguWindowScrollListener: NguWindowScrollListener,
private _nguCarouselHammerManager: NguCarouselHammerManager
) {
super();
this._setupButtonListeners();
Expand Down Expand Up @@ -347,45 +339,48 @@ export class NguCarousel<T>
}

ngOnDestroy() {
this._hammertime?.destroy();
this._hammer?.destroy();
this._destroy$.next();
}

/** Get Touch input */
private _setupHammer(): void {
from(import('hammerjs'))
// Note: the dynamic import is always a microtask which may run after the view is destroyed.
// `takeUntil` is used to prevent setting Hammer up if the view had been destroyed before
// the HammerJS is loaded.
.pipe(takeUntil(this._destroy$))
.subscribe(() => {
const hammertime = (this._hammertime = new Hammer(this._touchContainer.nativeElement));
hammertime.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });
// Note: doesn't need to unsubscribe because streams are piped with `takeUntil` already.
this._nguCarouselHammerManager
.createHammer(this._touchContainer.nativeElement)
.subscribe(hammer => {
this._hammer = hammer;

hammertime.on('panstart', () => {
hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });

this._nguCarouselHammerManager.on(hammer, 'panstart').subscribe(() => {
this.carouselWidth = this._nguItemsContainer.nativeElement.offsetWidth;
this.touchTransform = this.transform[this.deviceType!]!;
this.dexVal = 0;
this._setStyle(this._nguItemsContainer.nativeElement, 'transition', '');
});

if (this.vertical.enabled) {
hammertime.on('panup', (ev: any) => {
this._nguCarouselHammerManager.on(hammer, 'panup').subscribe((ev: any) => {
this._touchHandling('panleft', ev);
});
hammertime.on('pandown', (ev: any) => {

this._nguCarouselHammerManager.on(hammer, 'pandown').subscribe((ev: any) => {
this._touchHandling('panright', ev);
});
} else {
hammertime.on('panleft', (ev: any) => {
this._nguCarouselHammerManager.on(hammer, 'panleft').subscribe((ev: any) => {
this._touchHandling('panleft', ev);
});
hammertime.on('panright', (ev: any) => {

this._nguCarouselHammerManager.on(hammer, 'panright').subscribe((ev: any) => {
this._touchHandling('panright', ev);
});
}
hammertime.on('panend pancancel', (ev: any) => {
if (Math.abs(ev.velocity) >= this.velocity) {
this.touch.velocity = ev.velocity;

this._nguCarouselHammerManager.on(hammer, 'panend pancancel').subscribe(({ velocity }) => {
if (Math.abs(velocity) >= this.velocity) {
this.touch.velocity = velocity;
let direc = 0;
if (!this.RTL) {
direc = this.touch.swipe === 'panright' ? 0 : 1;
Expand All @@ -403,10 +398,11 @@ export class NguCarousel<T>
this._setStyle(this._nguItemsContainer.nativeElement, 'transform', '');
}
});
hammertime.on('hammer.input', ev => {

this._nguCarouselHammerManager.on(hammer, 'hammer.input').subscribe(({ srcEvent }) => {
// allow nested touch events to no propagate, this may have other side affects but works for now.
// TODO: It is probably better to check the source element of the event and only apply the handle to the correct carousel
ev.srcEvent.stopPropagation();
srcEvent.stopPropagation();
});
});
}
Expand Down