Scorrimento continuo

Questa implementazione a scorrimento continuo è progettata per garantire che non ci siano mai variazioni del layout, indipendentemente dal tempo impiegato dal server per rispondere con nuovi contenuti.

Uno dei problemi più comuni di molte implementazioni di scorrimento continuo è che il piè di pagina (o un elemento UX simile) viene spostato più in basso nella pagina ogni volta che vengono aggiunti nuovi elementi. Con questa implementazione a scorrimento continuo, non si verifica mai questo problema.

Approccio di alto livello

Quando possibile, i nuovi articoli vengono inseriti nella pagina prima che l'utente li raggiunga. Poiché questo inserimento avviene fuori schermo (e non è visibile all'utente), l'utente non riscontra variazioni del layout.

Nel caso in cui non sia possibile inserire in tempo nuovi contenuti, viene visualizzato un pulsante "Mostra altro". Tuttavia, il pulsante è abilitato solo quando nuovi elementi sono pronti per essere visualizzati. Ciò garantisce che l'utente non faccia clic sul pulsante solo per scoprire che non accade nulla. Di conseguenza, indipendentemente dalla lentezza con cui il server risponde ai nuovi contenuti (o dalla velocità con cui l'utente scorre la pagina), non ci saranno mai variazioni di layout impreviste.

Implementazione

L'API Intersection eventr è un modo efficace per monitorare la posizione e la visibilità degli elementi della pagina. Questo progetto viene implementato utilizzando due distinti osservatori di interserzione:

  • listObserver osserva la posizione dell'elemento #infinite-scroll-button alla fine dell'elenco a scorrimento continuo. Quando il pulsante si avvicina all'area visibile, i contenuti non inseriti vengono aggiunti al DOM.
  • sentinelObserver osserva la posizione dell'elemento #sentinel. Quando il sentinel diventa visibile, il server richiede più contenuti. La regolazione della posizione della sentinella è un modo per controllare con quanto anticipo devono essere richiesti nuovi contenuti al server.

Questo non è l'unico modo per affrontare le variazioni di layout derivanti dall'uso dello scorrimento continuo. Altri modi per affrontare questo problema includono il passaggio all'impaginazione, la virtualizzazione degli elenchi e la regolazione dei layout di pagina.

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();