Last active
September 21, 2023 15:18
-
-
Save ezzabuzaid/b5f1f494200698845a5a76a315ad502d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
BehaviorSubject, | |
Observable, | |
ObservableInput, | |
Subject, | |
debounceTime, | |
exhaustMap, | |
filter, | |
finalize, | |
fromEvent, | |
pipe, | |
startWith, | |
takeUntil, | |
tap, | |
} from 'rxjs'; | |
type InfinityScrollDirection = 'horizontal' | 'vertical'; | |
export interface InfinityScrollOptions<T> { | |
/** | |
* The element that is scrollable. | |
*/ | |
element: HTMLElement; | |
/** | |
* A BehaviorSubject that emits true when loading and false when not loading. | |
*/ | |
loading: BehaviorSubject<boolean>; | |
/** | |
* Indicates how far from the end of the scrollable element the user must be before the loadFn is called. | |
*/ | |
threshold: number; | |
/** | |
* The initial page index to start loading from. | |
*/ | |
initialPageIndex: number; | |
/** | |
* The direction of the scrollable element. | |
*/ | |
scrollDirection?: InfinityScrollDirection; | |
/** | |
* The function that is called when the user scrolls to the end of the scrollable element with respect to the threshold. | |
*/ | |
loadFn: (result: InfinityScrollResult) => ObservableInput<T>; | |
} | |
export interface InfinityScrollResult { | |
/** | |
* The next page index. | |
*/ | |
pageIndex: number; | |
} | |
function infinityScroll<T extends any[]>(options: InfinityScrollOptions<T>) { | |
const ensureScrolled = pipe( | |
filter(() => !options.loading.value), // ignore scroll event if already loading | |
debounceTime(100), // debounce scroll event to prevent lagginess on heavy scroll pages | |
filter(() => { | |
const remainingDistance = calculateRemainingDistance( | |
options.element, | |
options.scrollDirection | |
); | |
return remainingDistance <= options.threshold; | |
}) | |
); | |
const fetchData = pipe( | |
exhaustMap((_, index) => { | |
options.loading.next(true); | |
return options.loadFn({ | |
pageIndex: options.initialPageIndex + index, | |
}); | |
}), | |
tap(() => options.loading.next(false)), | |
// stop loading if error or explicitly completed (no more data) | |
finalize(() => options.loading.next(false)) | |
); | |
return fromEvent(options.element, 'scroll').pipe( | |
startWith(null), | |
ensureScrolled, | |
fetchData | |
); | |
} | |
function isScrollable( | |
element: HTMLElement, | |
direction: InfinityScrollDirection = 'vertical' | |
) { | |
if (direction === 'horizontal') { | |
return element.scrollWidth > element.clientWidth; | |
} else { | |
return element.scrollHeight > element.clientHeight; | |
} | |
} | |
function calculateRemainingDistanceToBottom(element: HTMLElement) { | |
const scrollPosition = element.scrollTop; | |
const clientHeight = element.clientHeight; | |
const totalHeight = element.scrollHeight; | |
return totalHeight - (scrollPosition + clientHeight); | |
} | |
function calculateRemainingDistanceOnXAxis(element: HTMLElement): number { | |
const scrollPosition = Math.abs(element.scrollLeft); | |
const clientWidth = element.clientWidth; | |
const totalWidth = element.scrollWidth; | |
return totalWidth - (scrollPosition + clientWidth); | |
} | |
function calculateRemainingDistance( | |
element: HTMLElement, | |
direction: InfinityScrollDirection = 'vertical' | |
) { | |
if (direction === 'horizontal') { | |
return calculateRemainingDistanceOnXAxis(element); | |
} else { | |
return calculateRemainingDistanceToBottom(element); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Please provide some example how to implement this in angular component.