Skip to content

Commit

Permalink
fix: set up Hammer outside of Angular to reduce CDs (#443)
Browse files Browse the repository at this point in the history
  • Loading branch information
arturovt authored Apr 18, 2023
1 parent 044a294 commit 9428647
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 34 deletions.
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

0 comments on commit 9428647

Please sign in to comment.