No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Infinite Scrolling: limitando la carga de datos

12/20
Recursos

Aportes 15

Preguntas 1

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

o inicia sesión.

Para implementar el infine scrolling en todas las secciones, lo que hice fue lo siguiente:

  1. Crear (en main.js) una función llamada getPaginatedMovies que reciba todos los posibles parámetros y realice la lógica de chequear los condicionales y hacer el llamado a la API de la siguiente página de resultados:
async function getPaginatedMovies(
    endPoint, 
    {
        categoryId, 
        query
    } = {},
    ) {
    const { 
        scrollTop, 
        scrollHeight, 
        clientHeight 
    } = document.documentElement;

    const scrollAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 15);
    const pageIsNotMax = page < maxPage;

    if (scrollAtBottom && pageIsNotMax) {
        page++;

        const { data } = await api(endPoint, {
            params: {
                page,
                with_genres: categoryId,
                query,
            },
        });
        const movies = data.results;

        printMoviePosters(movies, genericSection, true);
    } 
}
  1. (en main.js) Creé una función específica de paginación para cada sección, en las que que llamaba a la función global de paginación, y si era necesario volvía a sacar la info del hash para pasarla por parámetro:
function getPaginatedTrendingMovies() {
    getPaginatedMovies('/trending/movie/day');
}

function getPaginatedMoviesByCategory() {
    const [_, categoryData] = location.hash.split('=');
    const [categoryId] = categoryData.split('-');
    getPaginatedMovies('/discover/movie', {categoryId});
}

function getPaginatedMoviesBySearch() {
    const [_, undecodedQuery] = location.hash.split('=');
    const query = decodeURI(undecodedQuery);
    getPaginatedMovies('/search/movie', {undefined, query});
}
  1. (en navigation.js) La función de cada página asigna ahora a infiniteScroll su función propia de paginación:
function searchPage () {
   // función recortada
    infiniteScroll = getPaginatedMoviesBySearch;
}

function categoriesPage () {
   // función recortada
    infiniteScroll = getPaginatedMoviesByCategory;
}

function trendsPage () {
   // función recortada
    infiniteScroll = getPaginatedTrendingMovies;
}
  1. Luego me dí cuenta que si navegaba entre diferentes secciones, al compartir función page mantenía acumulado el último número en el que quedaba, por lo que si llegaba a la página 4 en trendings, al cargar una categoría y scrollear, no me cargaba la página 2 sino la 5, y así sucesivamente.
    Por lo tanto, (en navigation.js) reasigné a 1 la varible page antes de asignarle la función a infiniteScroll:
    page = 1;
    infiniteScroll = getPaginatedTrendingMovies;

De esta forma logré que las 3 secciones tengan infinite scrolling, intentando mantenerme DRY

*Cualquier error o correción, siempre súper bienvenido!

Yo resolví el problema obteniendo el id desde el hash

location.hash.split('=')[1].split('-')[0]

cree una funcion paga searMovies y una funcion para moviesByCategory y desde ahi antes de hacer la peticion tome del hash el id que necesite,

const [_, urlId, urlName] = location.hash.split("=");
async function getPageCategoryMovies() {
    const {scrollTop, clientHeight, scrollHeight} = document.documentElement;

    const scrollIsButton = (scrollTop + clientHeight) >= (scrollHeight - 25);

    const [_, urlId, urlName] = location.hash.split("=");
    const limitMaxPage = page < limitPage;

    if (scrollIsButton && limitMaxPage) {
        page++;
        const { data } = await api('discover/movie', {
            params : {
                with_genres : urlId,
                page,
            },
        });
    
        const movies = data.results;
    
        loadImage(genericSection, movies, {lazy: true, clear: false,});
    }

}

y lo mismo para search

En mi caso hice una función única para manejar todos los botones de carga infinita, de search, de categories y de trending.

/**
 * Maneja toda la lógica de los botones de carga.
 * 
 * @param {string} url La url del endpoint.
 * @param {string} currentPage La página actual que se recibe de la respuesta del endpoint.
 * @param {string} totalPages La cantidad total de páginas que se recibe del endpoint.
 * @param {string} container El contenedor principal o el section que está visible.
 * @param {object} extraParams Parámetros extra para mandar a la consulta.
 */
function loadMoreMoviesBtnHandler( url, currentPage, totalPages, container, extraParams ) {
    
    // Revisa si hay paginas para cargar.
    if (currentPage < totalPages) {

        // Crea el boton de carga y lo inserta unicamente en la seccion actual.
        const loadMoreBtn = document.createElement('button');

        loadMoreBtn.classList.add('loadMore');
        loadMoreBtn.innerText = 'Load More';
        loadMoreBtn.setAttribute('data-page', currentPage);

        // Agrega un observador al boton para disparar un evento de clic cuando sea visible.
        loadMoreMoviesObserver.observe(loadMoreBtn);

        container.appendChild(loadMoreBtn);

        // Agrega un event listener al boton.
        loadMoreBtn.addEventListener('click', async () => {
            let newCurrentPage = parseInt(loadMoreBtn.getAttribute('data-page')) + 1;

            const {data} = await api(url, {
                params: {
                    page: newCurrentPage,
                    ...extraParams
                }}
            );

            createMoviesMarkup(data.results, container, false);

            let lastNode = container.querySelector('.movie-container:last-child');
            lastNode.parentNode.insertBefore(loadMoreBtn, lastNode.nextSibling);
            loadMoreBtn.setAttribute('data-page', newCurrentPage);
        })
    }
    else {
        let loadMore = container.querySelectorAll('.loadMore');

        if ( loadMore.length ) {
            loadMore[0].remove();
        }
    }
}

También creé un Observer en vez de un eventListener de scroll (me pareció mas eficiente)

let loadMoreMoviesObserver = new IntersectionObserver((loadMoreObjetcs) => {
    loadMoreObjetcs.forEach(loadMoreButton => {
        
        if (loadMoreButton.isIntersecting) {
            loadMoreButton.target.click();
        }
    })
})

Y para llamar al handler lo hice así:

// Trending movies.
async function getTrendingMovies() {
    const endPoint = '/trending/movie/day';
    const {data} = await api(endPoint);

    const movies = data.results;

    createMoviesMarkup(movies, genericSection);
    loadMoreMoviesBtnHandler(endPoint, data.page, data.total_pages, genericSection);
}

// Search
async function getMoviesBySearch(query) {
    const endPoint = '/search/movie';
    const {data} = await api(endPoint, {
        params: {
            query,
        }
    });

    const movies = data.results;

    createMoviesMarkup(movies, genericSection);
    loadMoreMoviesBtnHandler(endPoint, data.page, data.total_pages, genericSection, {query});
}

// Categories
async function getMoviesByCategory(id) {
    const endPoint = '/discover/movie';
    const {data} = await api(endPoint, {
        params: {
            with_genres: id,
        }
    });

    const movies = data.results;

    createMoviesMarkup(movies, genericSection);
    loadMoreMoviesBtnHandler(endPoint, data.page, data.total_pages, genericSection, {with_genres: id});
}

Cuando intenté implementar el infinite scrolling por mi cuenta (de una forma distinta a la del Profesor Juan), también pensé que una buena forma de aplicarlo para todas las demás secciones sería mediante clousures para que así individualmente cada sección “recuerde” o “guarde” la página actual en la que se encuentra . Sin embargo… Al final ni siquiera me molesté en intentarlo debido a que en realidad (a pesar de que las conozco) no sé cómo crear ni usar las clousures, je je je 😅.
·
Por lo tanto al final terminé implementando todo de forma la cual dejaré por aquí~ Probablemente no sea la forma más bonita ni óptima, pero al menos funciona, je je 👉👈.
·
En mi solución, modifiqué las funciones getTrendingMovies, getMoviesBySearch y getMoviesByCategory para que recibieran un parámetro extra: el número de la página. De esta forma, cada vez que se llegue al final del scroll, simplemente se vuelve a llamar a las mismas funciones pero pasándoles la página que se supone deben cargar.

// Get Trending Movies.
async function getTrendingMovies(page = 1) {
	try {
		thereAreSomeRequestsInProcess = true;
		const isTheFirstLoad = (page === 1);
		
		if (isTheFirstLoad) {
			genericSection.innerHTML = "";
			showMoviesLoadingScreen(genericSection);
			
			const { data } = await api(`trending/movie/day?page=${page}`);
			const movies = data.results;
			currentLimitPagination = data.total_pages;
			createMovies(movies, genericSection, isTheFirstLoad);

			console.group("Respuestas del Servidor (GET Trending Movies)");
				console.log(data);
			console.groupEnd();
		} else {
			if (page <= currentLimitPagination) {
				showInfiniteScrollingMoviesLoadingScreen(genericSection);
				
				currentPagination = page;
				const { data } = await api(`trending/movie/day?page=${page}`);
				const movies = data.results;
				currentLimitPagination = data.total_pages;
				createMovies(movies, genericSection, isTheFirstLoad);
				if (page === currentLimitPagination) showInfiniteScrollingEndMessage(genericSection);
				
				console.group("Respuestas del Servidor (GET Trending Movies by Scrolling)");
					console.log(data);
				console.groupEnd();
			}
		}
		
		thereAreSomeRequestsInProcess = false;
		
	} catch (error) {
		console.group("%cError (GET Trending Movies)", consoleErrorMessageStyle);
			console.error(error);
		console.groupEnd();
		alert("Ocurrió un Error en el GET de las Películas en Tendencia.");
	}
}

·
¿Y quién se encarga de pasar el número de la página a cargar? Una función ubicada en el navigation.js donde también están las variables goblales que indican la página actual de la sección y el límite máximo de la misma.

// Infinite Scrolling

let currentPagination = 1; // Página Actual de la Sección
let currentLimitPagination = null; // Límite de la Paginación Actual
let thereAreSomeRequestsInProcess = false;

document.addEventListener("scroll", loadMoreMoviesByInfiniteScrolling);
function loadMoreMoviesByInfiniteScrolling() {
	const {scrollTop, scrollHeight, clientHeight} = document.documentElement;
	const endOfScrollReached = (scrollTop+clientHeight) >= (scrollHeight-100);
	if (endOfScrollReached && !thereAreSomeRequestsInProcess)  {
		thereAreSomeRequestsInProcess = true;
		
		if (location.hash === "#trends") {
			console.log(`Se llegó al final del scroll de la página ${currentPagination} en las tendencias.`);
			getTrendingMovies(currentPagination+1);
		}
		
		if (location.hash.startsWith("#search=")) {
			console.log(`Se llegó al final del scroll de la página ${currentPagination} en la búsqueda.`);
			const searchedTerm = location.hash.split("=")[1].trim();		
			getMoviesBySearch(searchedTerm, currentPagination+1);
		}
		
		if (location.hash.startsWith("#category=")) {
			console.log(`Se llegó al final del scroll de la página ${currentPagination} en la categoría.`);
			const categoryId = location.hash.split("=")[1].split("-")[0];
			getMoviesByCategory(categoryId, currentPagination+1);
		}
	}
}

Yo implemente la solucion de obtener los parametros, ya sea el ID o el query, a traves de location.hash. Exactamente como lo hicimos en navigation.js.

No se si sea lo mas optimo pero funciona.

yo resolvi el problema asi:

infiniteScroll = () => {
    getPaginatedMoviesBySearch(realQuery)}```

A mi parecer es menos complicado de lo que parece, ya que, al cargar la 1ra pagina de nuestra seccion (seach o category) con location.hash podemos tomar ese id o ese query para hacer la solucitud de la 2da pagina y de las sucesivas. Espero haber sido claro y que pueda servirle a alguien!!

En mi solucion me sali de la tangente sin querer al elegir como primer funcion para implementar el infinite scrolling aquella que maneja las peliculas recomendadas dentro de la seccion movieDetail y me encontre con una serie de complicaciones a resolver sobre la marcha:
-La primera fue el hecho de que esta llamada a la API no debe utilizar la variable infiniteScrolling que definimos para los demas eventos de scroll debido a que trabaja con el scroll vertical de un contenedor y no con el scroll de la seccion visible en la que nos encontramos.
-La segunda fue que comprendi que en la clase previa nunca establecimos un reset en la variable “page”, por lo cual cada vez que la reutilizabamos en un ciclo de uso cotidiano de un usuario (entrar a una seccion, scrollear, salir, entrar a otra seccion, scrollear, etc) no arrancabamos desde la primer pagina disponible de la API, por lo que tuve que buscar el punto justo donde insertarle un reset sin que ello produzca problemas en el infinite scroll en si.
-Y por ultimo me parecia muy poco comodo el hecho de tener que arrastrar el scroll del contenedor “manualmente” para cargar mas peliculas debido a que eso provocaba muchas veces que la funcion se ejecutara varias veces consecutivas sin darme posibilidad de corregir la posicion del scroll y de esa forma evitar las multiples (si bien esto es algo a corregir en futuros updates), por lo cual termine implementado otro eventListener que se encargar de proveernos el scroll vertical con la rueda del mouse, tal cual lo hacemos en otras secciones.
A continuacion les dejo como quedaria el codigo:

Asi queda la navigation de la seccion movieDetails

function movieDetailsPage () {
    console.log("Movie!!")
    headerSection.classList.add('header-container--long');
    //headerSection.style.background = '';
    arrowBtn.classList.remove('inactive');
    arrowBtn.classList.add('header-arrow--white');
    headerTitle.classList.add('inactive');
    headerCategoryTitle.classList.add('inactive');
    searchForm.classList.add('inactive');

    trendingPreviewSection.classList.add('inactive');
    categoriesPreviewSection.classList.add('inactive');
    genericSection.classList.add('inactive');
    movieDetailSection.classList.remove('inactive');

    const [_, movieId] = location.hash.split('=') // ["#movie", 'movieID']
    getMovieDetailById(movieId);
    getRelatedMoviedById(movieId);
    //Aqui insertamos el reset de la pagina en esta funcion y en cualquier otra que haga uso del infiniteScroll
    page = 1;
}

Los eventos a escuchar dentro del contenedor

relatedMoviesContainer.addEventListener('scroll', getPaginatedRelatedMoviesbyId)
relatedMoviesContainer.addEventListener('wheel', scrollHorizontalContainer)

La funcion en si de llamada a la API

async function getPaginatedRelatedMoviesbyId() {
    const [_, movieId] = location.hash.split('=')
    const { scrollLeft, scrollWidth, clientWidth } = relatedMoviesContainer;

    const scrollIsEnd = (scrollLeft + clientWidth) >= (scrollWidth - 10);
    if (scrollIsEnd) {
        page++;
        const { data } = await api(urlSimilarMovies(movieId), {
            params: {
                page,
            }
    });
        const similarMovies = data.results;
        if (similarMovies) {
        movieRender(similarMovies, relatedMoviesContainer, {lazyLoad: true, clean: false});
        }
    }
}

Y por ultimo la funcion de scrollVertical haciendo uso del mouse

function scrollHorizontalContainer (event) {
	// Se puede modificar la velocidad corrigiendo el decimal
	relatedMoviesContainer.scrollLeft += event.deltaY * 0.50;
}

**Disclaimer
Hice uso del endpoint similarMovies debido a que testee muchas peliculas que no sean del todo populares (o son antiguas, etc) y el endpoint de Recommendations normalmente se encuentra vacio y por lo tanto no util para el uso de nuestra app

Yo lo resolví pero el código es un desastre 😂

function infiniteScroll() {
	const scrollCond =
		document.documentElement.scrollTop + document.documentElement.clientHeight >= document.documentElement.scrollHeight;
	if (scrollCond && location.hash.startsWith("#trends")) {
		getTrendingMovies({ pag: pag, clear: false });
		pag++;
	}

	if (scrollCond && location.hash.startsWith("#category=")) {
		const hashContent = location.hash;
		const idAndName = hashContent.slice(10);
		const idAndNameArray = idAndName.split("-");
		const clicked_id = idAndNameArray[0];
		const clicked_name = idAndNameArray[1].replace("%20", " ");

		getMoviesByCategory(clicked_id, clicked_name, pag, false);
		pag++;
	}

	if (scrollCond && location.hash.startsWith("#search=")) {
		const hashContent = location.hash;
		const [_, hashContentArray] = hashContent.split("=");
		const searchValue = hashContentArray.replaceAll("%20", " ");
		searchMovies(searchValue, { pag: pag, clear: false });
		pag++;
	}
}

Yo usé generators, me quedó así (está hecho rápido, todavía tengo que limpiar el código)

Navigation.js

function printMoviesByGenre() {
    genre.classList.add('active')
    const regex = /category=(\d+)-(.*)/
    const genreData = regex.exec(location.hash)
    const id = genreData[1]
    const name = genreData[2]

    container = moviesByGenre
    getMoviesByGenre({ id, name })
    const page = generator(1)

    document.onscroll = () =>
        getPaginatedMovies('discover/movie', { with_genres: id }, page)
}

Index.js

let maxPage
let container
let page = 1
function* generator(i) {
    while (true) {
        i++
        yield i
    }
}
//...
async function getPaginatedMovies(endpoint, params = {}, pageGenerator) {
    if (page > maxPage) return
    const { scrollTop, scrollHeight, clientHeight } = document.documentElement
    const scrollIsInBottom = scrollTop + clientHeight >= scrollHeight - 30

    if (scrollIsInBottom) {
        page = pageGenerator.next().value
        params.page = page
        const { data } = await api(endpoint, {
            params,
        })
        const movies = data.results
        createCards(movies, container, false)
    }
}

Este es mi aporte, lo realicé antes de ver la clase 11 pero lo del passive no lo sabía XD, así que lo agregué después. En esta implementación creo el escuchador de scroll y mando a llamar la función inifityScroll en la cual se detectará en que vista se está parado y si se llegó al máximo scroll posible según cada condicional.

/* PAGINATION */
let page = 1; //Para las listas de trending y popular
window.addEventListener("scroll", infinityScroll, {passive : false});
function infinityScroll(){
    //Se extrae el máximo scroll segúnla vista actual
    const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
    if( (maxScroll === window.scrollY) && (location.hash === "#more-trends")){
        //Se aumenta la página
        page++;
        //Se manda a llamar la función de consulta
        trendingMovieViewMore();
        
    }
    if( (maxScroll === window.scrollY) && (location.hash === "#more-popular")){
        //Se aumenta la página
        page++;
        //Se manda a llamar la función de consulta
        popularMovieViewMore();
    }
    if( (maxScroll === window.scrollY) && (location.hash.startsWith("#category"))){
        //Se obtiene el id y nombre de categoría del hash usando split
        let [vista, categoryIdName] = location.hash.split("=");
        const [categoryId, categoryName] = categoryIdName.split("_");
        //Se aumenta la página
        page++;
        //Se manda a llamar la función de consulta
        getMovieByCategory({name: categoryName, id:categoryId});
    }
    if( (maxScroll === window.scrollY) && (location.hash.startsWith("#search"))){
        //Se obtiene el nombre de búsqueda en el hash usando split y join
        let [vista, searchName] = location.hash.split("=");
        let query = searchName.split("-");
        query = query.join(" ");
        //Se aumenta la página
        page++;
        //Se manda a llamar la función de consulta
        getMovieBySearch({query});
    }   
}

En las funciones de consulta como trendingviewmore() etc; cada una tiene el parámetro de page dentro de la consulta de axios. Adicional se agregan las siguientes
líneas para limpiar el contenedor de los posters de películas cuando es primera vez que se entra a dicha vista.

if(page <= 1)
   cardsContainer.innerHTML = ""; 

En navigation.js cada función de cambio de vista reiniciará la variable global “page” igualándola a 1, por ejemplo:

function trendingListPage(){
  
 //Se quitan las vistas que no se deben mostrar y se deja sólo la deseada
    category.classList.add("d-none");
    movieDetail.classList.add("d-none");
    movieDetail.classList.add("d-md-none");
    searching.classList.add("d-none");
    trending.classList.add("d-none");
    popular.classList.add("d-none");
    popularList.classList.add("d-none");
    searchBar.classList.add("d-none");
    trendingList.classList.remove("d-none");
    //Se reinicia la paginación
    page = 1;
    //Se manda a llamar las funciones generadoras de la información  
    trendingMovieViewMore();   
    window.scrollTo(0, 0);
}

aunque ese reinicio se puede hacer dentro de la función navigator también, ya que el objetivo es detectar un cambio de vista y reiniciar la paginación.

Como bonus cree un botón para hacer scroll hacia el top de las vistas ya que es dificil scrollear tanto luego de cargar mucha información y funciona con este código:

/* SCROLLTOP BUTTON */
const scrollTop = document.getElementById("scroll-button");
scrollTop.addEventListener("click", () =>{
    window.scrollTo(0, 0);
})

Mi solución para el Infinite Scrolling en páginas/secciones que reciben parámetros fue guardar estos valores en variables globales (como hicimos con maxPage) para luego poder acceder a ellos desde la función de Infinite Scrolling.

  1. Creé dos nuevas variables globales.
  • endpointInfiniteScroll: para guardar el endpoint y no tener que crear una función por cada sección
  • infiniteScrollParams: para guardar los parámetros como la categoría, búsqueda, etc.
// navigation.js
let page = 1;
let maxPage;
let infiniteScroll;
let endpointInfiniteScroll;
let infiniteScrollParams = {
  params: {
    page: 1,
  },
};
  1. Luego, desde la primera petición en la función GET asigné el endpoint y los parámetros a las variables globales ya inicializadas.
// main.js 
async function getMoviesByCategory(id) {
  endpointInfiniteScroll = "/discover/movie?with-genres=";
  infiniteScrollParams = {
    params: {
      with_genres: id,
    },
  }

  const { data } = await api(endpointInfiniteScroll , infiniteScrollParams);

  maxPage = data.total_pages;

  createMovies(data.results, genericListMoviesPreview);

}
  1. Después, en la función del Infinite Scrolling usé las variables globales para realizar la petición según los parámetros y endpoint que se usaron para el primer GET.
//main.js 
async function getInfiniteMoviesList() {
  const { scrollTop, scrollHeight, clientHeight } = document.documentElement;

  const scrollIsBottom = (scrollTop + clientHeight >= scrollHeight - 15);

  const pageIsNotMax = (page < maxPage);

  if (scrollIsBottom && pageIsNotMax) {

    page++;

    infiniteScrollParams.params.page = page;
    
    const { data } = await api(endpointInfiniteScroll, infiniteScrollParams);

    createMovies(data.results, genericListMoviesPreview, {
      cleanSection: false,
    });

  }
  1. En la función “navigator” para cada vez que cambiemos de sección establecí un valor inicial por defecto para estas variables, así, en secciones que no se necesiten estos valores no causen errores o se vayan acumulando.
//navigation.js
function navigator() {

  if(infiniteScroll){
    window.removeEventListener('scroll', infiniteScroll, {passive: false});
    infiniteScroll = undefined;
    page = 1;
    maxPage = 0;
    endpointInfiniteScroll = undefined;
    infiniteScrollParams = {
      params: {
        page: 1,
      },
    };
  }

}
  1. Por último, en la página o sección que queramos usar el Infinite Scrolling asignamos su función a la variable global de “infiniteScroll”.
//navigation.js

function categoriesPage() {
// función recortada
  infiniteScroll = getInfiniteMoviesList;
}

La solución es crear esas funciones reutiilizables para que pueda usarse con págiina 1 hasta la página n, también desde una función promesa podemos retornar un valor específico y cuando la usemos podemos catcharla con .then y allí hacer lo que queramos

const getTrendingMovies = async (page = 1) => {
    const dataUtil = {};
    try {
        const { status, data } = await api.get(URL_TRENDING_RES('movie', 'day'), {
            params: { page },
        });
        ...
        dataUtil.total_pages = data.total_pages;
    } catch (error) {
        ...
    }
    return dataUtil;
};

Para catcharla

const trendsPage = () => {
    ...
    getTrendingMovies().then((data) => {
        if (data.total_pages === 1) removeInfiniteScroll();
    });
    // Scroll infinito
    infiniteScroll = () => {
        if (scrollIsOnThreshold()) {
            getTrendingMovies(page).then((data) => {
                if (page >= data.total_pages) removeInfiniteScroll();
            });
        }
    };
};

.
.
Mi propuesta, commit actual: Click aquí

Para no tener problemas con los parámetros, lo que hice es que esas variables que recibe se hagan globales…por ejemplo en el caso de searchMovies:

De tal manera que así se puede aplicar la lógica del scroll infinito al igual que en trendingMovies