En el emocionante mundo del desarrollo web, uno de los temas populares es la implementación de Infinite Scrolling. Sin embargo, hay detalles cruciales que atender, como limitar la carga de datos al implementar paginación. Aquí te explicaremos cómo abordar este desafío utilizando la API de The MovieDB.
¿Qué información ofrece la API de The MovieDB?
La API de The MovieDB nos proporciona una gran cantidad de información valiosa. Además de los datos de películas, nos informa sobre:
Cantidad de resultados: total de elementos obtenidos.
Cantidad de páginas: número máximo de páginas disponibles para explorar.
El objetivo es utilizar esta información para establecer un límite en el proceso de Infinite Scrolling, de modo que no se siga solicitando interminablemente más datos cuando ya se ha alcanzado la última página.
¿Cómo implementar la limitación de páginas?
Para aplicar esta limitación, primero debemos obtener el número máximo de páginas desde nuestro primer llamado a la API y almacenarlo en una variable. Así evitamos que la aplicación intente seguir cargando después de alcanzar la última página permitida.
Aquí tienes un ejemplo de cómo podrías implementar esto en tu código:
let maxPage;// Variable para almacenar la página máxima// Función para obtener las películas con paginaciónfunctionGetTrendingMovies(){ axios.get('URL_DE_LA_API').then(data=>{console.log(data.totalPages);// Visualizar total de páginas maxPage = data.totalPages;// Asignar el número máximo de páginas});}// Función para cargar más películasfunctionGetPaginatedTrendingMovies(){let page =1;// Página inicialconstpageIsMax=()=> page >= maxPage;if(!pageIsMax()){ page++;// Lógica para cargar más datos}}
¿Cómo evaluar que el scroll infinito funciona adecuadamente?
Para asegurar que la funcionalidad de Infinite Scrolling no falla, es importante verificar dos condiciones:
Que el scroll haya llegado al final de la página.
Que la página actual sea menor que maxPage.
Solo si ambas condiciones se cumplen, se permitirá cargar más datos y avanzar a la siguiente página.
¿Cuál es el siguiente reto?
El próximo desafío es implementar esta lógica de paginación en otras funciones como Get Movies by Search o Get Movies by Category. Te instamos a que pruebes este reto, aunque este último utiliza parámetros que presentan un obstáculo adicional. No temas, aprender enfrentando estas problemáticas elevará tus habilidades como desarrollador.
Este conocimiento no solo te brinda las herramientas para mejorar la experiencia del usuario al evitar errores de carga infinita, sino que además optimiza el rendimiento de tu aplicación al evitar llamadas innecesarias a la API. ¡No te desanimes! Sigue perfeccionando tus habilidades y afrontando nuevos retos en el desarrollo web.
Para implementar el infine scrolling en todas las secciones, lo que hice fue lo siguiente:
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:
(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:
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:
++De esta forma logré que las 3 secciones tengan infinite scrolling, intentando mantenerme DRY++
*Cualquier error o correción, siempre súper bienvenido!
Además, agregué un mensaje cada vez que se queda sin resultados el scroll:
Para hacer esto, creé (en index.html) una nueva sección debajo de genericList, inactiva por defecto:
<section id="max-page"class="inactive"><h3>We're sorry but there's no more matches for your search 😕
</h3></section>
En nodes.js definí la variable:
const maxPageReached =$('#max-page');
En la función global de paginación agregué un else if que chequeara que se llegó al final del scroll y que no hay más páginas de resultados:
asyncfunctiongetPaginatedMovies(endPoint,{ categoryId, query
}={},){// función recortada}elseif(scrollAtBottom &&!pageIsNotMax){ maxPageReached.classList.remove('inactive');// removemos el inactive y de esta forma se muestra el mensaje};}
Finalmente, en navigation.js, a la función de cada página volví a ponerle inactive a la magPageReached para que no aparezca en caso de que se active en algún momento y se mantenga en las otras secciones, como en el siguiente ejemplo:
Agregué algunos estilos para que se vea más o menos decente (sé que se puede mejorar):
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.
*/functionloadMoreMoviesBtnHandler(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}=awaitapi(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 =newIntersectionObserver((loadMoreObjetcs)=>{ loadMoreObjetcs.forEach(loadMoreButton=>{if(loadMoreButton.isIntersecting){ loadMoreButton.target.click();}})})
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.asyncfunctiongetTrendingMovies(page =1){try{ thereAreSomeRequestsInProcess =true;const isTheFirstLoad =(page ===1);if(isTheFirstLoad){ genericSection.innerHTML="";showMoviesLoadingScreen(genericSection);const{ data }=awaitapi(`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 }=awaitapi(`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 Scrollinglet currentPagination =1;// Página Actual de la Secciónlet currentLimitPagination =null;// Límite de la Paginación Actuallet thereAreSomeRequestsInProcess =false;document.addEventListener("scroll", loadMoreMoviesByInfiniteScrolling);functionloadMoreMoviesByInfiniteScrolling(){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);}}}
Acabo de notar que el valor de page crece demasiado tras un pequeño scroll. Yo creí que iba a incrementar de a 1 tras mostrar 20 nuevos resultados, pero no es así. No encuentro el error. A alguien más le pasa?
Disculpa, Ruben. No entiendo tu pregunta. ¿Cuál es el error?
Hola, @RubenSH. :D
Sé a cuál error te refieres, se supone que a medida que vamos haciendo scroll vamos cargando una página de la API a la vez, pero, en cambio, cargan más de una.
A mi parecer es un "error" entre comillas, ya que no afecta el funcionamiento del proyecto. Quizás baja un poco la performance de la web, pero no lo sé aún ya que no lo he testeado.
De todas formas puedes solucionarlo con una función que retrase o haga una pequeña parada antes de continuar con su ejecución. Yo lo hice con una promesa, te dejó el código.
Yo creé una función de paginación general que se puede usar con cualquier sección, y esta va a recibir los paramatros que necesite para crear el Infinite Scrolling. Para usarla simplemente se tiene que invocar dentro de la función inicial de cada sección.
//Esta funcion se usa para mostrar mas resultados de peliculas para las vistas de tendencias, categorias y búsqueda.asyncfunctiongetPaginatedMovies({ url, params, page, container
}={}){const{ data }=awaitapi(url,{params:{...params, page,}});//se desestructura la respuesta de api para obtener los datos de una vezconst movies = data.results;//movies es el objeto de peliculas según los datos iniciales. tiene una total de 20 elementos.console.log(movies);//se llama a la funcion createMovies para visualizar las peliculas según los datos inicialescreateMovies({ movies, container,movieModificator:'--small',clean:false,})}
Ejemplo de implementación:
asyncfunctiongetMovieBySearch(query){let page =1;//Esta variable controla la páginación de la API.//se hace la solicitud GET con la instancia de AXIOS para traer el objeto de peliculas según la búsqueda del usuario.const{ data }=awaitapi(`search/movie`,{params:{'language':'en-US', query,}});//se desestructura la respuesta de api para obtener los datos de una vezconst movies = data.results;//movies es el objeto de peliculas según la categoria. tiene una total de 20 elementos.//se llama a la funcion createMovies para visualizar las peliculas segun la búsqueda del usuario.createMovies({ movies,container: genericMovieList,movieModificator:"--small",})//Cargar mas contenido//primero se valida que no exista una funcion infinite scrolling, y si si lo hay se elimina.if(infiniteScrolling){ genericListSection.removeEventListener('scroll', infiniteScrolling); infiniteScrolling =undefined;console.log('test');}//Se asigna la función especifíca de la sección al infinite scrolling.infiniteScrolling=()=>{const isUserAtBottom = genericListSection.scrollTop+ genericListSection.clientHeight>= genericListSection.scrollHeight-5;//Se hace la validación si el usuario alcanzó el fondo de la pantallaif(isUserAtBottom){ page++;//Se suma uno a la página//Se invoca la función de paginación con los respectivos parametros de la sección.getPaginatedMovies({url:'search/movie',params:{'language':'en-US', query,}, page,container: genericMovieList,})}} genericListSection.addEventListener('scroll', infiniteScrolling);}
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.
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
functionmovieDetailsPage(){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;}
Y por ultimo la funcion de scrollVertical haciendo uso del mouse
functionscrollHorizontalContainer(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 usé generators, me quedó así (está hecho rápido, todavía tengo que limpiar el código)
Navigation.js
functionprintMoviesByGenre(){ 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 =1function*generator(i){while(true){ i++yield i
}}//...asyncfunctiongetPaginatedMovies(endpoint, params ={}, pageGenerator){if(page > maxPage)returnconst{ scrollTop, scrollHeight, clientHeight }=document.documentElementconst scrollIsInBottom = scrollTop + clientHeight >= scrollHeight -30if(scrollIsInBottom){ page = pageGenerator.next().value params.page= page
const{ data }=awaitapi(endpoint,{ params,})const movies = data.resultscreateCards(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 popularwindow.addEventListener("scroll", infinityScroll,{passive:false});functioninfinityScroll(){//Se extrae el máximo scroll segúnla vista actualconst 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 consultatrendingMovieViewMore();}if((maxScroll ===window.scrollY)&&(location.hash==="#more-popular")){//Se aumenta la página page++;//Se manda a llamar la función de consultapopularMovieViewMore();}if((maxScroll ===window.scrollY)&&(location.hash.startsWith("#category"))){//Se obtiene el id y nombre de categoría del hash usando splitlet[vista, categoryIdName]=location.hash.split("=");const[categoryId, categoryName]= categoryIdName.split("_");//Se aumenta la página page++;//Se manda a llamar la función de consultagetMovieByCategory({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 joinlet[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 consultagetMovieBySearch({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:
functiontrendingListPage(){//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:
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.
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.
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.
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.
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
hice un refactor de pues estaba repitiendo codigo por cada peticion que se hacia
si ya se estan cargando datos, no se hacen mas peticiones, por eso coloco que si isLoading es true no genere otro llamado
pero seguido, isLoading pasa de nuevo a true para hacer una nueva peticion
luego de cada forEach, page se incrementa a 1 para pasar a la siguiente pagina y isLoading pasa a false para que no este haciendo peticiones cada vez que se genere el scroll
en la funcion que cree para el scroll infinito
pongo parametros con un valor null para que sea llamado segun la funcion que se ejecute en cada vista del navigation y hago la validacion para generar el scroll
Finalmente hago el llamado en cada funcion por cada vista
Hola platzinautas 😁, la solución que encontre fue la siguiente:
1ro - definí una variable global infiniteScrollParams:
let ``infiniteScrollParams;
2do - luego la agregué en la validación de la funcion Navigator() para poder resetearla al cambiar de página:
3ro - al final de cada una de mis funciones de navegación le asigne el valor de la función necesaria a la variable infiniteScroll, y de necesitar parametros se los asigno a la variable global infiniteScrollParams:
Me tomo un buen tiempito , espero les sirva mi solucion.🥵
Para hacer a la función getPaginatedTrendingMovies un poco más general, use el evento 'onsroll', en vez de la variable infiniteScroll, asi podemos enviar parametros, que en este caso sería la url de la seccion en la se mande a llamar a la función.