Desplazamiento infinito

Esta implementación de desplazamiento infinito está diseñada para garantizar que nunca haya ningún cambio de diseño, independientemente del tiempo que le lleve al servidor responder con contenido nuevo.

Uno de los problemas más comunes con muchas implementaciones de desplazamiento infinito es que el pie de página (o un elemento de UX similar) se desplaza más abajo cada vez que se agregan elementos nuevos. Con esta implementación de desplazamiento infinito, esto nunca ocurre.

Enfoque de alto nivel

Siempre que sea posible, se insertan elementos nuevos en la página antes de que el usuario los acceda. Como esta inserción se realiza fuera de la pantalla (y el usuario no puede verla), el usuario no experimenta cambios de diseño.

En el caso de que no se pueda insertar contenido nuevo a tiempo, se mostrará el botón “Mostrar más”. Sin embargo, el botón se habilita solo cuando se muestran elementos nuevos, lo que garantiza que el usuario no haga clic en el botón solo para descubrir que no sucede nada. Por lo tanto, independientemente de qué tan lento responda el servidor con el contenido nuevo (o qué tan rápido se desplace el usuario), nunca habrá cambios de diseño inesperados.

Implementación

La API de Intersection Observer es una forma eficaz de supervisar la posición y la visibilidad de los elementos de página. Este diseño se implementa utilizando dos observadores de intercalación separados:

  • listObserver observa la posición del #infinite-scroll-button que se encuentra al final de la lista de desplazamiento infinito. Cuando el botón se acerca al viewport, se agrega contenido no insertado al DOM.
  • sentinelObserver observa la posición del elemento #sentinel. Cuando el centinela se vuelve visible, se solicita más contenido al servidor. Ajustar la posición del centinela es una forma de controlar con cuánta anticipación se debe solicitar contenido nuevo desde el servidor.

Esta no es la única forma de abordar los cambios de diseño que surgen del uso del desplazamiento infinito. Otras formas de abordar este problema incluyen cambiar a la paginación, usar la virtualización de listas y ajustar los diseños de página.

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