echen
echen

Reputation: 2072

Infinite Scrolling of <table /> element in react.js

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

The 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

Answers (1)

echen
echen

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

Related Questions