Unendliches Scrollen

Diese Implementierung von unendlichem Scrollen sorgt dafür, dass es nie zu Layoutverschiebungen kommt – unabhängig davon, wie lange der Server braucht, um mit neuen Inhalten zu antworten.

Eines der häufigsten Probleme bei Implementierungen von unendlichem Scrollen besteht darin, dass die Seitenfußzeile (oder ein ähnliches UX-Element) weiter nach unten verschoben wird, wenn neue Elemente hinzugefügt werden. Bei der Implementierung des unendlichen Scrollens kommt das nie vor.

Allgemeiner Ansatz

Wann immer möglich, werden neue Artikel auf der Seite eingefügt, bevor der Nutzer sie erreicht. Da das Einfügen außerhalb des Bildschirms erfolgt (und für den Nutzer nicht sichtbar ist), gibt es für den Nutzer keine Layoutverschiebungen.

Falls neue Inhalte nicht rechtzeitig eingefügt werden können, wird stattdessen die Schaltfläche „Mehr anzeigen“ angezeigt. Die Schaltfläche wird jedoch nur aktiviert, wenn neue Elemente für die Anzeige bereit sind. Dadurch wird sichergestellt, dass der Nutzer nicht auf die Schaltfläche klickt, nur um festzustellen, dass nichts passiert. Unabhängig davon, wie langsam der Server mit neuen Inhalten reagiert (oder wie schnell der Nutzer scrollt), gibt es keine unerwarteten Layoutverschiebungen.

Implementierung

Mit der Intersection Observer API lässt sich die Position und Sichtbarkeit von Seitenelementen effizient überwachen. Dieses Design wird mithilfe von zwei separaten Intersertion-Beobachtern implementiert:

  • listObserver beobachtet die Position von #infinite-scroll-button, die sich am Ende der Liste mit unendlichem Scrollen befindet. Wenn sich die Schaltfläche dem Darstellungsbereich nähert, werden dem DOM nicht eingefügte Inhalte hinzugefügt.
  • sentinelObserver beobachtet die Position des #sentinel-Elements. Wenn der Sentinel sichtbar wird, werden mehr Inhalte vom Server angefordert. Durch das Anpassen der Position des Sentinel kann gesteuert werden, wie weit im Voraus neue Inhalte vom Server angefordert werden sollen.

Dies ist nicht die einzige Möglichkeit, Layoutverschiebungen anzugehen, die auf die Verwendung von unendlichem Scrollen zurückzuführen sind. Weitere Möglichkeiten, dieses Problem anzugehen, sind der Wechsel zur Paginierung, die Listenvirtualisierung und das Anpassen von Seitenlayouts.

HTML

<div id="infinite-scroll-container">
    <div id="sentinel"></div>
    <div class="item">A</div>
    <div class="item">B</div>
    <div class="item">C</div>
    <div class="item">D</div>
    <div class="item">E</div>
    <button id="infinite-scroll-button" disabled>
        <span class="disabled-text">Loading more items...</span>
        <span class="active-text">Show more</span>
    </button>
</div>

CSS


        :root {
    --active-button-primary: #0080ff;
    --active-button-font:#ffffff;
    --disabled-button-primary: #f5f5f5;
    --disabled-button-secondary: #c4c4c4;
    --disabled-button-font: #000000;
}
#infinite-scroll-container {
    position: relative;
}
#sentinel {
    position: absolute;
    bottom: 150vh;
}
#infinite-scroll-button {
    cursor: pointer;
    border: none;
    padding: 1em;
    width: 100%;
    font-size: 1em;
}
#infinite-scroll-button:enabled {
    color: var(--active-button-font);
    background-color: var(--active-button-primary)
}
#infinite-scroll-button:disabled {
    color: var(--disabled-button-font);
    background-color: var(--disabled-button-primary);
    cursor: not-allowed;
    animation: 3s ease-in-out infinite loadingAnimation;
}
#infinite-scroll-button:enabled .disabled-text {
    display: none;
}
#infinite-scroll-button:disabled .active-text {
    display: none;
}
@keyframes loadingAnimation {
    0% {
        background-color: var(--disabled-button-primary);
    }
    50% {
        background-color: var(--disabled-button-secondary);
    }
    100% {
        background-color: var(--disabled-button-primary);
    }
}
        

JS


        function infiniteScroll() {
    let responseBuffer = [];
    let hasMore;
    let requestPending = false;
    const loadingButtonEl = document.querySelector('#infinite-scroll-button');
    const containerEl = document.querySelector('#infinite-scroll-container');
    const sentinelEl = document.querySelector("#sentinel");
    const insertNewItems = () => {
        while (responseBuffer.length > 0) {
            const data = responseBuffer.shift();
            const el = document.createElement("div");
            el.textContent = data;
            el.classList.add("item");
            el.classList.add("new");
            containerEl.insertBefore(el, loadingButtonEl);
            console.log(`inserted: ${data}`);
        }
        sentinelObserver.observe(sentinelEl);
        if (hasMore === false) {
            loadingButtonEl.style = "display: none";
            sentinelObserver.unobserve(sentinelEl);
            listObserver.unobserve(loadingButtonEl);
        }
        loadingButtonEl.disabled = true
    }
    loadingButtonEl.addEventListener("click", insertNewItems);
    const requestHandler = () => {
        if (requestPending) return;
        console.log("making request");
        requestPending = true;
        fakeServer.fakeRequest().then((response) => {
            console.log("server response", response);
            requestPending = false;
            responseBuffer = responseBuffer.concat(response.items);
            hasMore = response.hasMore;
            loadingButtonEl.disabled = false;;
        });
    }
    const sentinelObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.intersectionRatio > 0) {
                observer.unobserve(sentinelEl);
                requestHandler();
            }
        });
    });
    const listObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.intersectionRatio > 0 && entry.intersectionRatio < 1) {
                insertNewItems();
            }
        });
    }, {
        rootMargin: "0px 0px 200px 0px"
    });
    sentinelObserver.observe(sentinelEl);
    listObserver.observe(loadingButtonEl);
}
infiniteScroll();