Reputation: 2072
I have a particularly interesting problem with respect to infinite scrolling using react.js.
The goal here is to make sure no matter how large a table becomes, we only ever let render()
return a fixed subset of all rows.
We will let lowerVisualBound
and upperVisualBound
denote the subset of rows to render()
onto the DOM.
Note these bounds are set to be larger than the viewport, causing a scrollbar to appear.
We will modify lowerVisualBound
and upperVisualBound
as the user scrolls.
Here, we further denote
height
as the height of the visible portion of the tabletotalHeight
as the height of the entire table (that includes all rows between lowerVisualBound
and upperVisualBound
scrollTop
and state.lastScrollTop
as the current and previous scroll top respectivelyThe Following Snippet Kind of Does the Trick - Except the scrollbar itself does not change position after additional data has been loaded (i.e. upper or lower
VisualBound reset). This causes the user's view of the data to jump.
const rowDisplayBoundry = 2 * this.props.pageSize;
if (scrollTop < this.state.lastScrollTop && scrollTop <= 0.0 * totalHeight) {
// up scroll limit triggered
newState.lowerVisualBound = Math.max(this.state.lowerVisualBound - this.props.pageSize, 0);
newState.upperVisualBound = newState.lowerVisualBound + rowDisplayBoundry;
} else if (scrollTop > this.state.lastScrollTop && (scrollTop + height) > totalHeight) {
// down scroll limit triggered
newState.upperVisualBound = this.state.upperVisualBound + this.props.pageSize;
newState.lowerVisualBound = newState.upperVisualBound - rowDisplayBoundry;
}
// TODO now what do we set scrollTop to, post these mutations? (presumably using a setTimeout())
Can anyone suggest an algorithm to compute a new scrollTop
such that changing the visual bound preserve the user's view?
Note I believe this should be theoretically possible because the # of rows between upper & lower visual bound is set to be > what can be displayed in the viewport. Therefore, after each mutation in those bounds, the user does not lose any rows that he was viewing immediately before the mutation. It is only a matter of computing the correct location the scrollbar post-mutation.
Upvotes: 2
Views: 2284
Reputation: 2072
The following appears to have worked ... though not sure if there are corner cases where it doesn't (please excuse the rather liberal use of jQuery selector for this demostration)
handleScroll: function (e) {
const $target = $(e.target);
const scrollTop = $target.scrollTop();
const height = $target.height();
const totalHeight = $target.find("tbody").height();
const avgRowHeight = totalHeight / (this.state.upperVisualBound - this.state.lowerVisualBound);
/**
* always update lastScrollTop on scroll event - it helps us determine
* whether the next scroll event is up or down
*/
var newState = {lastScrollTop: scrollTop};
/**
* we determine the correct display boundaries by keeping the distance between lower and upper visual bound
* to some constant multiple of pageSize
*/
const rowDisplayBoundry = 2 * this.props.pageSize;
if (scrollTop < this.state.lastScrollTop && scrollTop <= 0) {
// up scroll limit triggered
newState.lowerVisualBound = Math.max(this.state.lowerVisualBound - this.props.pageSize, 0);
newState.upperVisualBound = newState.lowerVisualBound + rowDisplayBoundry;
// if top most rows reached, do nothing, otherwise reset scrollTop to preserve current view
if (!(newState.lowerVisualBound === 0))
setTimeout(function () {
$target.scrollTop(Math.max(scrollTop + this.props.pageSize * avgRowHeight, 0));
}.bind(this));
} else if (scrollTop > this.state.lastScrollTop && (scrollTop + height) >= totalHeight) {
// down scroll limit triggered
newState.upperVisualBound = this.state.upperVisualBound + this.props.pageSize;
newState.lowerVisualBound = newState.upperVisualBound - rowDisplayBoundry;
setTimeout(function () {
// TODO ensure that new scrollTop doesn't trigger another load event
// TODO ensure this computationally NOT through flagging variables
$target.scrollTop(scrollTop - this.props.pageSize * avgRowHeight);
}.bind(this));
}
this.setState(newState);
}
Upvotes: 1