I have data which consists of thousands of items that I want to be able to display. However, displaying all items at the same time would significantly weight down performance. Also, I only really need to view a few items at a time.

So basically, what I want is something that behaves like a Facebook news feed, Twitter timeline or any instant messenger chat history view. In all those examples, initially, I'm only being shown a few items and by scrolling to the end of the list, I can dynamically load more.

I can also jump to a specific position of the feed, like a certain date and time of a chat history. The feed usually doesn't load and show every single message from now until the specified history position, but it jumps to the position and shows a few surrounding messages. Again, by reaching (either) end of the list, more items are dynamically added.

What is this feature called? Which library can I use to implement it?

2

There are 2 best solutions below

0
finefoot On

Here is a small proof-of-concept that doesn't require any 3rd-party libraries.

In this example, the database consist of 501 items from 0 to 500. FeedEngine is taking care of the mechanics that add/remove items from the list. The actual site/item content is managed by an itemCallback function. Here, it is called customItemBuilder and is kept very simple: it only displays the item index and alternates the background color. This is where you would actually pull the contents for that specific itemIndex from your database and dynamically adjust the itemElement accordingly.

On mobile, you can swipe through the items. On desktop, it's best to use the mouse wheel to scroll through the feed.

<!DOCTYPE html>
<html lang="en">

<head>
<script>
/**
 * FeedEngine
 *
 * FeedEngine is a vertical news feed or timeline implementation. Initially,
 * only a certain, small amount of items are displayed. If the user reaches
 * either end of the container, for example by scrolling, more and more items
 * are dynamically added to the feed as required. It's also possible to jump
 * to a specific item, i.e. feed position.
 *
 * For each item, an empty, blank DIV element will be added to the container
 * element. Afterwards, a function is called which receives two parameters:
 * `itemElement`, the new element, and `itemIndex`, the index of the new
 * item. This callback function allows you to customize the presentation of
 * the feed items.
 *
 * Options:
 *     containerElement - The element which will contain all DIV elements for
 *         the items. For best results, you should probably choose a DIV
 *         element for the container as well. Furthermore, its CSS should
 *         contain something like `overflow: scroll`. Note: Its attributes
 *         `innerHTML` and `onscroll` will be overwritten.
 *     itemCallback - This function will be called after a new item has been
 *         added to the container. If the callback doesn't return `true`, the
 *         item will immediately be removed again.
 *     moreItemsCount -  The number of new items that will be added above and
 *         below the first item, the target item of a jump or the outermost
 *         item in the feed, respectively.
 *     moreItemsTrigger - The threshold distance to the outermost item which
 *         triggers more items to be added to the feed. For example, if this
 *         option is set to `0`, new items will only be added once the
 *         outermost item is fully in view. Furthermore, a value greater than
 *         or equal to `moreItemsCount` doesn't make sense.
 *     inverseOrder - Use bottom-to-top instead of top-to-bottom order.
 *
 * @constructor
 * @param {Object} options - Options object.
 */
function FeedEngine(options) {
    'use strict';
    this.itemCallback = (itemElement, itemIndex) => {};
    this.moreItemsCount = 20;
    this.moreItemsTrigger = 5;
    this.inverseOrder = false;
    Object.assign(this, options);
    if (this.containerElement === undefined) {
        throw new Error('container element must be specified');
    }
    this.jumpToItem = (itemIndex) => {
        this.containerElement.innerHTML = '';
        this.topItemIndex = itemIndex;
        this.bottomItemIndex = itemIndex;
        var initialItem = this.insertItemBelow(true);
        for (var i = 0; i < this.moreItemsCount; i++) {
            this.insertItemAbove();
            this.insertItemBelow();
        }
        this.containerElement.scrollTop = initialItem.offsetTop - this.containerElement.offsetTop + (this.inverseOrder ? initialItem.clientHeight - this.containerElement.clientHeight : 0);
    };
    this.insertItemAbove = () => {
        this.topItemIndex += this.inverseOrder ? 1 : -1;
        var itemElement = document.createElement('div');
        this.containerElement.insertBefore(itemElement, this.containerElement.children[0]);
        if (!this.itemCallback(itemElement, this.topItemIndex)) {
            itemElement.remove();
        }
        return itemElement;
    };
    this.insertItemBelow = (isInitialItem) => {
        if (isInitialItem === undefined || !isInitialItem) {
            this.bottomItemIndex += this.inverseOrder ? -1 : 1;
        }
        var itemElement = document.createElement('div');
        this.containerElement.appendChild(itemElement);
        if (!this.itemCallback(itemElement, this.bottomItemIndex)) {
            itemElement.remove();
        }
        return itemElement;
    };
    this.itemVisible = (itemElement) => {
        var containerTop = this.containerElement.scrollTop;
        var containerBottom = containerTop + this.containerElement.clientHeight;
        var elementTop = itemElement.offsetTop - this.containerElement.offsetTop;
        var elementBottom = elementTop + itemElement.clientHeight;
        return elementTop >= containerTop && elementBottom <= containerBottom
    };
    this.containerElement.onscroll = (event) => {
        var topTriggerIndex = this.moreItemsTrigger;
        var bottomTriggerIndex = event.target.children.length - this.moreItemsTrigger - 1;
        var topTriggerElement = event.target.children[topTriggerIndex];
        var bottomTriggerElement = event.target.children[bottomTriggerIndex];
        var topTriggerVisible = this.itemVisible(topTriggerElement);
        var bottomTriggerVisible = this.itemVisible(bottomTriggerElement);
        for (var i = 0; i < this.moreItemsCount; i++) {
            if (topTriggerVisible) {
                this.insertItemAbove();
            }
            if (bottomTriggerVisible) {
                this.insertItemBelow();
            }
        }
    };
    this.jumpToItem(0);
}
</script>
</head>

<body>
Feed:
<button onclick="feed = new FeedEngine({containerElement: document.getElementById('container'), itemCallback: customItemBuilder})">top-to-bottom</button>
<button onclick="feed = new FeedEngine({containerElement: document.getElementById('container'), itemCallback: customItemBuilder, inverseOrder: true})">bottom-to-top</button>
<input type="text" id="jump" value="250">
<button onclick="feed.jumpToItem(parseInt(document.getElementById('jump').value))">jump</button>
<div id="container" style="overflow: scroll; width: 300px; height: 100px; resize: both;"></div>
<script>
function customItemBuilder(itemElement, itemIndex) {
    if (0 <= itemIndex && itemIndex <= 500) {
        /* customize the item DIV element here */
        itemElement.innerHTML = 'Content for item index ' + itemIndex;
        itemElement.style.backgroundColor = itemIndex % 2 ? 'LightCyan' : 'LightGray';
        return true;
    }
}
window.onload = () => {
    document.getElementsByTagName('button')[0].click();
}
</script>
</body>

</html>

1
Amit Ranjan On

The feature is called Infinite Scrolling