Skip to content

Instantly share code, notes, and snippets.

@ezzabuzaid
Last active September 21, 2023 15:18
Show Gist options
  • Save ezzabuzaid/b5f1f494200698845a5a76a315ad502d to your computer and use it in GitHub Desktop.
Save ezzabuzaid/b5f1f494200698845a5a76a315ad502d to your computer and use it in GitHub Desktop.
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);
}
}
@anandu06
Copy link

Please provide some example how to implement this in angular component.

@ezzabuzaid
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment