From 7880627955765d8d50575f75e5a9a79b4bb8bb85 Mon Sep 17 00:00:00 2001 From: Oren Farhi Date: Sun, 7 Jan 2018 12:58:26 +0200 Subject: [PATCH] [RELEASE] 0.8.0 --- .gitignore | 3 +- CHANGELOG.md | 3 + package.json | 2 +- src/models.ts | 44 ++++++-- src/modules/infinite-scroll.directive.ts | 25 ++-- src/ngx-infinite-scroll.ts | 2 - src/services/event-trigger.ts | 19 +--- src/services/scroll-register.ts | 128 ++++++++++----------- src/services/scroll-resolver.ts | 9 +- tests/services/event-trigger.spec.ts | 138 +++++++++++++---------- tests/services/scroll-register.spec.ts | 107 ++++++++++-------- tsconfig-build.json | 2 +- tsconfig.json | 1 + 13 files changed, 264 insertions(+), 219 deletions(-) diff --git a/.gitignore b/.gitignore index a3b10588..e03ab969 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ $ cat .gitignore *.log *.tgz -npm-debug.* \ No newline at end of file +npm-debug.* +examples/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fba17a0c..8b793dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## v 0.8.1 (2018/01/07) +* [REFACTOR] - performance optimization for scroll events + ## v 0.8.0 (2018/01/02) * [FIX] - now triggers only once when in or after target (#200) * [REFACTOR] - "distance" number has been refined to be the percentage point of the scroll nob. diff --git a/package.json b/package.json index 53696df8..5aa21db9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngx-infinite-scroll", - "version": "0.8.0", + "version": "0.8.1", "description": "An infinite scroll directive for Angular compatible with AoT compilation and Tree shaking", "main": "./bundles/ngx-infinite-scroll.umd.js", "module": "./modules/ngx-infinite-scroll.es5.js", diff --git a/src/models.ts b/src/models.ts index 3c4cf3f3..e92752df 100644 --- a/src/models.ts +++ b/src/models.ts @@ -16,18 +16,9 @@ export interface IPositionStats { scrolledUntilNow: number; totalToScroll: number; } - -export interface IScrollerConfig { - distance: { - down: number; - up: number; - }; - scrollParent?: ContainerRef; -} - -export interface IScrollStats { - isScrollingDown: boolean; - shouldScroll: boolean; +export interface IScrollerDistance { + down?: number; + up?: number; } export interface IScrollState { @@ -42,3 +33,32 @@ export interface IResolver { isWindow: boolean; axis: any; } + +export interface IScrollRegisterConfig { + container: ContainerRef; + throttle: number; +} + +export interface IScroller { + fromRoot: boolean; + horizontal: boolean; + disable: boolean; + throttle: number; + scrollWindow: boolean; + element: ElementRef; + scrollContainer: string | ElementRef; + alwaysCallback: boolean; + downDistance: number; + upDistance: number; +} + +export interface IScrollParams { + isScrollingDown: boolean; + shouldFireScrollEvent: boolean; + positionStats: IPositionStats; +} + +export interface IInfiniteScrollAction { + type: string; + payload: InfiniteScrollEvent; +} diff --git a/src/modules/infinite-scroll.directive.ts b/src/modules/infinite-scroll.directive.ts index 3fd6d615..7bb7f7af 100644 --- a/src/modules/infinite-scroll.directive.ts +++ b/src/modules/infinite-scroll.directive.ts @@ -12,9 +12,9 @@ import { } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; -import { InfiniteScrollEvent } from '../models'; +import { InfiniteScrollEvent, IInfiniteScrollAction } from '../models'; import { hasWindowDefined, inputPropChanged } from '../services/ngx-ins-utils'; -import { createScroller } from '../services/scroll-register'; +import { createScroller, InfiniteScrollActions } from '../services/scroll-register'; @Directive({ selector: '[infiniteScroll], [infinite-scroll], [data-infinite-scroll]' @@ -69,22 +69,29 @@ export class InfiniteScrollDirective disable: this.infiniteScrollDisabled, downDistance: this.infiniteScrollDistance, element: this.element, - events: { - // tslint:disable-next-line:arrow-parens - down: event => this.zone.run(() => this.scrolled.emit(event)), - // tslint:disable-next-line:arrow-parens - up: event => this.zone.run(() => this.scrolledUp.emit(event)) - }, horizontal: this.horizontal, scrollContainer: this.infiniteScrollContainer, scrollWindow: this.scrollWindow, throttle: this.infiniteScrollThrottle, upDistance: this.infiniteScrollUpDistance - }); + }).subscribe((payload: any) => this.zone.run(() => this.handleOnScroll(payload))); }); } } + handleOnScroll({ type, payload }: IInfiniteScrollAction) { + switch (type) { + case InfiniteScrollActions.DOWN: + return this.scrolled.emit(payload); + + case InfiniteScrollActions.UP: + return this.scrolledUp.emit(payload); + + default: + return; + } + } + ngOnDestroy() { this.destroyScroller(); } diff --git a/src/ngx-infinite-scroll.ts b/src/ngx-infinite-scroll.ts index f405a90a..b8c6fcba 100644 --- a/src/ngx-infinite-scroll.ts +++ b/src/ngx-infinite-scroll.ts @@ -4,8 +4,6 @@ export { InfiniteScrollEvent, IPositionElements, IPositionStats, - IScrollStats, - IScrollerConfig, IResolver } from './models'; diff --git a/src/services/event-trigger.ts b/src/services/event-trigger.ts index 3800bf3d..3fe31dc8 100644 --- a/src/services/event-trigger.ts +++ b/src/services/event-trigger.ts @@ -23,18 +23,9 @@ export interface IScrollConfig { shouldFireScrollEvent: boolean; } -export function shouldTriggerEvents({ alwaysCallback, shouldFireScrollEvent }: IScrollConfig) { - return (alwaysCallback || shouldFireScrollEvent); -} - -export function triggerEvents( - callbacks: ITriggerEvents, - isScrollingDown: boolean, - scrolledUntilNow: number -) { - const eventData: InfiniteScrollEvent = { - currentScrollPosition: scrolledUntilNow - }; - const callback = isScrollingDown ? callbacks.down : callbacks.up; - callback(eventData); +export function shouldTriggerEvents( + alwaysCallback: boolean, + shouldFireScrollEvent: boolean, + isTriggeredCurrentTotal: boolean) { + return (alwaysCallback || shouldFireScrollEvent) && !isTriggeredCurrentTotal; } diff --git a/src/services/scroll-register.ts b/src/services/scroll-register.ts index 0352e21f..e266ea05 100644 --- a/src/services/scroll-register.ts +++ b/src/services/scroll-register.ts @@ -2,102 +2,96 @@ import 'rxjs/add/observable/fromEvent'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/mergeMap'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/do'; import 'rxjs/add/operator/sampleTime'; import { ElementRef } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { Subscription } from 'rxjs/Subscription'; +import { map } from 'rxjs/operator/map'; +import { of } from 'rxjs/observable/of'; -import { ContainerRef, IPositionStats, IScrollState } from '../models'; +import * as Models from '../models'; import { AxisResolver } from './axis-resolver'; -import { shouldTriggerEvents, triggerEvents } from './event-trigger'; +import { shouldTriggerEvents, IScrollConfig } from './event-trigger'; import { resolveContainerElement } from './ngx-ins-utils'; import { calculatePoints, createResolver } from './position-resolver'; import * as ScrollResolver from './scroll-resolver'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -export interface IScrollRegisterConfig { - container: ContainerRef; - throttleDuration: number; - mergeMap: Function; - scrollHandler: (value: any) => void; -} - -export interface IScroller { - fromRoot: boolean; - horizontal: boolean; - disable: boolean; - throttle: number; - scrollWindow: boolean; - element: ElementRef; - scrollContainer: string | ElementRef; - alwaysCallback: boolean; - downDistance: number; - upDistance: number; - events?: { - down: (ev) => any; - up: (ev) => any; - }; -} - -export function createScroller(config: IScroller): Subscription { +export function createScroller(config: Models.IScroller) { const { scrollContainer, scrollWindow, element, fromRoot } = config; const resolver = createResolver({ axis: new AxisResolver(!config.horizontal), windowElement: resolveContainerElement(scrollContainer, scrollWindow, element, fromRoot) }); const stats = calculatePoints(element, resolver); - const scrollState: IScrollState = { + const scrollState: Models.IScrollState = { lastScrollPosition: 0, lastTotalToScroll: 0, totalToScroll: stats.totalToScroll, isTriggeredTotal: false }; - const options: IScrollRegisterConfig = { + const options: Models.IScrollRegisterConfig = { container: resolver.container, - mergeMap: () => calculatePoints(element, resolver), - scrollHandler: (positionStats: IPositionStats) => - handleOnScroll(scrollState, positionStats, config), - throttleDuration: config.throttle + throttle: config.throttle + }; + const distance = { + up: config.upDistance, + down: config.downDistance }; - return attachScrollEvent(options); + return attachScrollEvent(options) + .mergeMap((ev: any) => of(calculatePoints(element, resolver))) + .map((positionStats: Models.IPositionStats) => + toInfiniteScrollParams(scrollState.lastScrollPosition, positionStats, distance)) + .do(({ positionStats }: Models.IScrollParams) => + ScrollResolver.updateScrollState( + scrollState, + positionStats.scrolledUntilNow, + positionStats.totalToScroll + )) + .filter(({ shouldFireScrollEvent }: Models.IScrollParams) => + shouldTriggerEvents(shouldFireScrollEvent, config.alwaysCallback, scrollState.isTriggeredTotal) + ) + .do(() => { + ScrollResolver.updateTriggeredFlag(scrollState, true); + }) + .map(toInfiniteScrollAction); } -export function attachScrollEvent( - options: IScrollRegisterConfig -): Subscription { - return Observable.fromEvent(options.container, 'scroll') - .sampleTime(options.throttleDuration) - .mergeMap((ev: any) => Observable.of(options.mergeMap(ev))) - .subscribe(options.scrollHandler); +export function attachScrollEvent(options: Models.IScrollRegisterConfig): Observable<{}> { + return Observable + .fromEvent(options.container, 'scroll') + .sampleTime(options.throttle); } -export function handleOnScroll( - scrollState: IScrollState, - positionStats: IPositionStats, - config: IScroller -) { - const distance = { - down: config.downDistance, - up: config.upDistance - }; +export function toInfiniteScrollParams( + lastScrollPosition: number, + positionStats: Models.IPositionStats, + distance: Models.IScrollerDistance +): Models.IScrollParams { const { isScrollingDown, shouldFireScrollEvent } = ScrollResolver.getScrollStats( - scrollState.lastScrollPosition, + lastScrollPosition, positionStats, - { distance } + distance ); - const scrollConfig = { - alwaysCallback: config.alwaysCallback, - shouldFireScrollEvent + return { + isScrollingDown, + shouldFireScrollEvent, + positionStats + }; +} + +export const InfiniteScrollActions = { + DOWN: '[NGX_ISE] DOWN', + UP: '[NGX_ISE] UP' +}; + +export function toInfiniteScrollAction(response: Models.IScrollParams): Models.IInfiniteScrollAction { + const { isScrollingDown, positionStats: { scrolledUntilNow: currentScrollPosition } } = response; + return { + type: isScrollingDown ? InfiniteScrollActions.DOWN : InfiniteScrollActions.UP, + payload: { + currentScrollPosition + } }; - ScrollResolver.updateScrollState(scrollState, positionStats.scrolledUntilNow, positionStats.totalToScroll); - const shouldTrigger = shouldTriggerEvents(scrollConfig); - if (shouldTrigger && !scrollState.isTriggeredTotal) { - ScrollResolver.updateTriggeredFlag(scrollState, true); - triggerEvents( - config.events, - isScrollingDown, - positionStats.scrolledUntilNow - ); - } } diff --git a/src/services/scroll-resolver.ts b/src/services/scroll-resolver.ts index 904956d1..59fe101a 100644 --- a/src/services/scroll-resolver.ts +++ b/src/services/scroll-resolver.ts @@ -1,11 +1,10 @@ -import { IPositionStats, IScrollerConfig, IScrollState } from '../models'; +import { IPositionStats, IScrollState, IScrollerDistance } from '../models'; export function shouldFireScrollEvent( container: IPositionStats, - config: IScrollerConfig, + distance: IScrollerDistance, scrollingDown: boolean ) { - const distance = config.distance; let remaining: number; let containerBreakpoint: number; if (scrollingDown) { @@ -30,11 +29,11 @@ export function isScrollingDownwards( export function getScrollStats( lastScrollPosition: number, container: IPositionStats, - config: IScrollerConfig + distance: IScrollerDistance ) { const isScrollingDown = isScrollingDownwards(lastScrollPosition, container); return { - shouldFireScrollEvent: shouldFireScrollEvent(container, config, isScrollingDown), + shouldFireScrollEvent: shouldFireScrollEvent(container, distance, isScrollingDown), isScrollingDown }; } diff --git a/tests/services/event-trigger.spec.ts b/tests/services/event-trigger.spec.ts index 46273cf8..0706a150 100644 --- a/tests/services/event-trigger.spec.ts +++ b/tests/services/event-trigger.spec.ts @@ -1,4 +1,4 @@ -import { IScrollerProps, shouldTriggerEvents, triggerEvents } from '../../src/services/event-trigger'; +import { IScrollerProps, shouldTriggerEvents } from '../../src/services/event-trigger'; const props = { alwaysCallback: true, @@ -9,64 +9,84 @@ const props = { } as IScrollerProps; describe('EventTrigger', () => { - it('should return true when alwaysCallback', () => { - const actual = shouldTriggerEvents({ - alwaysCallback: true, - shouldFireScrollEvent: false, - }); - expect(actual).toBeTruthy(); - }); - - it('should return true when alwaysCallback, shouldFireScrollEvent', () => { - const actual = shouldTriggerEvents({ - alwaysCallback: true, - shouldFireScrollEvent: true, - }); - expect(actual).toBeTruthy(); - }); - - it('should return true when not alwaysCallback, shouldFireScrollEvent is true', () => { - const actual = shouldTriggerEvents({ - alwaysCallback: false, - shouldFireScrollEvent: true, - }); - expect(actual).toBeTruthy(); - }); - - it('should return false when alwaysCallback, shouldFireScrollEvent is true', () => { - const actual = shouldTriggerEvents({ - alwaysCallback: true, - shouldFireScrollEvent: true, - }); - expect(actual).toBeTruthy(); - }); - - it('should return false when not alwaysCallback, shouldFireScrollEvent is true', () => { - const actual = shouldTriggerEvents({ - alwaysCallback: false, - shouldFireScrollEvent: true, - }); - expect(actual).toBeTruthy(); - }); - - describe('triggerEvents', () => { - let callbacks; - - beforeEach(() => { - callbacks = { - down: jasmine.createSpy('down'), - up: jasmine.createSpy('up') - }; - }); - - it('should trigger down event when scrolling down', () => { - triggerEvents(callbacks, true, 9); - expect(callbacks.down).toHaveBeenCalled(); - }); - - it('should trigger up event when scrolling up', () => { - triggerEvents(callbacks, false, 9); - expect(callbacks.up).toHaveBeenCalled(); + [ + { + it: 'should return TRUE when alwaysCallback', + params: { + alwaysCallback: true, + shouldFireScrollEvent: false, + isTriggeredTotal: false + }, + expected: true + }, + { + it: 'should return FALSE when alwaysCallback, isTriggeredTotal', + params: { + alwaysCallback: true, + shouldFireScrollEvent: false, + isTriggeredTotal: true + }, + expected: false + }, + { + it: 'should return TRUE when alwaysCallback, shouldFireScrollEvent', + params: { + alwaysCallback: true, + shouldFireScrollEvent: true, + isTriggeredTotal: false + }, + expected: true + }, + { + it: 'should return FALSE when alwaysCallback, shouldFireScrollEvent, isTriggeredTotal', + params: { + alwaysCallback: true, + shouldFireScrollEvent: true, + isTriggeredTotal: true + }, + expected: false + }, + { + it: 'should return TRUE when shouldFireScrollEvent ONLY', + params: { + alwaysCallback: false, + shouldFireScrollEvent: true, + isTriggeredTotal: false + }, + expected: true + }, + { + it: 'should return FALSE when shouldFireScrollEvent, isTriggeredTotal', + params: { + alwaysCallback: false, + shouldFireScrollEvent: true, + isTriggeredTotal: true + }, + expected: false + }, + { + it: 'should return FALSE when not alwaysCallback, shouldFireScrollEvent is false', + params: { + alwaysCallback: false, + shouldFireScrollEvent: false, + isTriggeredTotal: false + }, + expected: false + }, + { + it: 'should return FALSE when isTriggeredTotal ONLY', + params: { + alwaysCallback: false, + shouldFireScrollEvent: false, + isTriggeredTotal: true + }, + expected: false + } + ].forEach((spec) => { + it(spec.it, () => { + const { isTriggeredTotal, alwaysCallback, shouldFireScrollEvent } = spec.params; + const actual = shouldTriggerEvents(alwaysCallback, shouldFireScrollEvent, isTriggeredTotal); + expect(actual).toBe(spec.expected); }); }); }); diff --git a/tests/services/scroll-register.spec.ts b/tests/services/scroll-register.spec.ts index 3a3b355f..41eaba5f 100644 --- a/tests/services/scroll-register.spec.ts +++ b/tests/services/scroll-register.spec.ts @@ -1,15 +1,17 @@ -import { Subscription } from 'rxjs/Rx'; +import * as Models from '../../src/models'; +import { Observable } from 'rxjs/Observable'; import { async, inject } from '@angular/core/testing'; import * as ScrollRegister from '../../src/services/scroll-register'; +import * as ScrollResolver from '../../src/services/scroll-resolver'; +import * as EventTrigger from '../../src/services/event-trigger'; import { ElementRef } from '@angular/core'; describe('Scroll Regsiter', () => { let mockedElement: ElementRef; let mockedContainer: ElementRef; - // let scrollRegister: ScrollRegister; const createMockDom = () => { const container = document.createElement('section'); @@ -22,62 +24,71 @@ describe('Scroll Regsiter', () => { return { element: mockedElement, container: mockedContainer }; }; - beforeEach(() => { - // scrollRegister = new ScrollRegister(); - }); + // beforeEach(() => { + + // }); - it('should create a Subscription of scroll observable', () => { + it('should create a Observable of scroll observable', () => { const mockDom = createMockDom(); - const scrollConfig: ScrollRegister.IScrollRegisterConfig = { + const scrollConfig: Models.IScrollRegisterConfig = { container: mockDom.container.nativeElement, - mergeMap: (e: any) => e, - scrollHandler: (ev: any) => ev, - throttleDuration: 300, + throttle: 300, }; - const scroller$: Subscription = ScrollRegister.attachScrollEvent(scrollConfig); + const scroller$: Observable<{}> = ScrollRegister.attachScrollEvent(scrollConfig); const actual = scroller$; expect(actual).toBeDefined(); }); - // describe('Manage Scroll State', () => { - // it('should backup old Total and update the new one', () => { - // const state = { - // totalToScroll: 10, - // lastTotalToScroll: 0 - // } as any; - // const newTotal = 20; - // ScrollRegister.updateTotalToScroll(newTotal, state); - // const actual = state.totalToScroll; - // const expected = newTotal; - // expect(actual).toEqual(expected); - // }); + it('should create a scroll params object', () => { + const lastScrollPosition = 0; + const positionStats = {} as Models.IPositionStats; + const distance = { + down: 2, + up: 3, + } as Models.IScrollerDistance; + const scrollStats = { + isScrollingDown: true, + shouldFireScrollEvent: true + }; + spyOn(ScrollResolver, 'getScrollStats').and.returnValue(scrollStats); + const actual = ScrollRegister.toInfiniteScrollParams(lastScrollPosition, positionStats, distance); + const expected = 3; + expect(Object.keys(actual).length).toEqual(expected); + }); - // it('should return false when total != lastTotal', () => { - // const state = { - // totalToScroll: 10, - // lastTotalToScroll: 0 - // } as any; - // const actual = ScrollRegister.isSameTotalToScroll(state); - // expect(actual).toBeFalsy(); - // }); + describe('toInfiniteScrollAction', () => { + let response; - // it('should return true total = lastTotal', () => { - // const state = { - // totalToScroll: 10, - // lastTotalToScroll: 10 - // } as any; - // const actual = ScrollRegister.isSameTotalToScroll(state); - // expect(actual).toBeTruthy(); - // }); + beforeEach(() => { + response = { + positionStats: { + scrolledUntilNow: 100 + } + } as Models.IScrollParams; + }); - // it('should set the isTriggeredTotal', () => { - // const state = { - // isTriggeredTotal: false - // } as any; - // ScrollRegister.updateTriggeredFlag(state, true); - // const actual = state.isTriggeredTotal; - // expect(actual).toBeTruthy(); - // }); - // }); + [ + { + it: 'should trigger down event when scrolling down', + params: { + isScrollingDown: true + }, + expected: ScrollRegister.InfiniteScrollActions.DOWN + }, + { + it: 'should trigger up event when scrolling up', + params: { + isScrollingDown: false + }, + expected: ScrollRegister.InfiniteScrollActions.UP + } + ].forEach((spec) => { + it(spec.it, () => { + const params = { ...response, ...spec.params }; + const actual = ScrollRegister.toInfiniteScrollAction(params); + expect(actual.type).toBe(spec.expected); + }); + }); + }); }); diff --git a/tsconfig-build.json b/tsconfig-build.json index 401dfc6e..6e677322 100644 --- a/tsconfig-build.json +++ b/tsconfig-build.json @@ -7,7 +7,7 @@ "moduleResolution": "node", "outDir": "dist", "rootDir": ".", - // "inlineSourceMap": true, + "inlineSourceMap": false, "sourceMap": true, "inlineSources": true, "target": "es2015", diff --git a/tsconfig.json b/tsconfig.json index 052aa56d..79bee9ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "exclude": [ "node_modules", "example", + "examples", "dist" ] } \ No newline at end of file