Skip to content

Instantly share code, notes, and snippets.

@Phineas
Created July 22, 2020 06:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Phineas/3a2e05b79226a773e1a4284fe6552518 to your computer and use it in GitHub Desktop.
Save Phineas/3a2e05b79226a773e1a4284fe6552518 to your computer and use it in GitHub Desktop.
import React, { Component, ComponentType, ReactType, UIEvent } from 'react';
type VirtualizedScrollElementProps<
T,
B
> = {
childElement: T;
items: ExtractProps<T>[];
baseProps?: B;
prepend?: Element[];
}
type IVirtualizedScrollElement<
T extends ComponentType<ExtractProps<T>>,
B = any
> = Component<
VirtualizedScrollElementProps<
T,
B
>
>;
type VirtualizedScrollElementState<T> = {
visibleItems: ExtractProps<T>[];
setupDone: boolean;
}
type ExtractProps<TComponentOrTProps> =
TComponentOrTProps extends ComponentType<infer TProps>
? TProps
: TComponentOrTProps;
export default class VirtualizedScrollElement<
T extends ComponentType<ExtractProps<T>>,
B = any
> extends Component<
VirtualizedScrollElementProps<
T,
B
>,
VirtualizedScrollElementState<T>
> implements IVirtualizedScrollElement<
T,
B
> {
state: VirtualizedScrollElementState<T> = {
setupDone: false,
visibleItems: []
}
ref = React.createRef<HTMLDivElement>();
containerRef = React.createRef<HTMLDivElement>();
itemHeight: number = 0;
currentItemOffset: number = 0;
lastScrollTop: number = 0;
componentDidMount = () => {
const rect = this.ref.current?.firstElementChild?.getBoundingClientRect();
this.itemHeight = rect?.height || 0;
window.addEventListener('resize', () => {
this.calculateVisibleElements();
});
this.setState({ setupDone: true }, this.calculateVisibleElements);
}
componentDidUpdate = () => {
if (this.state.setupDone && this.containerRef.current) {
this.containerRef.current.onscroll = (e) => {
const { scrollTop } = e.target as HTMLDivElement;
console.log(scrollTop);
if (scrollTop > this.itemHeight * 5) {
this.currentItemOffset += Math.floor((scrollTop - (this.itemHeight * 5)) / this.itemHeight);
console.log(this.currentItemOffset);
this.lastScrollTop = scrollTop;
}
this.calculateVisibleElements();
}
}
}
calculateVisibleElements = () => {
console.log('updating');
if (this.containerRef.current) {
// calculate visible element amount
const visibleAmount = Math.ceil(this.containerRef.current.getBoundingClientRect().height / this.itemHeight + 2);
const visibleItems: ExtractProps<T>[] = [];
if (this.currentItemOffset + visibleAmount > this.props.items.length) this.currentItemOffset = this.props.items.length - visibleAmount;
for (let i = 0; i < visibleAmount; i++) {
visibleItems.push(this.props.items[i + this.currentItemOffset]);
}
this.setState({
setupDone: true,
visibleItems
}, () => {
if (this.lastScrollTop > this.itemHeight * 5) this.containerRef.current?.scrollTo({ top: this.lastScrollTop, behavior: 'auto' });
});
}
}
render = () => {
// if setup isnt done we just render a single element to get the height for calculations
if (!this.state.setupDone) {
return (
<div ref={this.ref} style={{
height: 'auto',
width: 'auto'
}}>
<this.props.childElement {...this.props.items[0]}/>
</div>
);
}
return (
<div ref={this.containerRef} {...this.props.baseProps}>
{this.state.visibleItems.map((props) => {
return <this.props.childElement {...props}/>
})}
</div>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment