<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Leonardo Alipazaga</title>
    <description>The latest articles on DEV Community by Leonardo Alipazaga (@leoalipazaga).</description>
    <link>https://dev.to/leoalipazaga</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F222064%2F2ad121ba-594d-4c42-9314-f143005502ba.jpeg</url>
      <title>DEV Community: Leonardo Alipazaga</title>
      <link>https://dev.to/leoalipazaga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leoalipazaga"/>
    <language>en</language>
    <item>
      <title>Infinite Scroll con Rxjs</title>
      <dc:creator>Leonardo Alipazaga</dc:creator>
      <pubDate>Wed, 18 Sep 2019 05:29:29 +0000</pubDate>
      <link>https://dev.to/leoalipazaga/infinite-scroll-con-rxjs-3mlg</link>
      <guid>https://dev.to/leoalipazaga/infinite-scroll-con-rxjs-3mlg</guid>
      <description>&lt;p&gt;Hace poco empecé a estudiar la famosa librería rxjs y me pareció realmente sorprendente su gran potencial para resolver features que a menudo como developer's nos enfrentamos. El scroll infinito es uno de esos features. En este post les explicaré como hacer un scroll infinito paso a paso usando rxjs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entonces, ¿Que necesitamos?
&lt;/h3&gt;

&lt;p&gt;Particularmente me agrada jsfiddle por su ligereza, sin embargo dejo a su libre elección el editor de texto con el que más se sientan a gusto (VSCode, SublimeText, CodePen, repl.it, etc.) . Psdta: tienen que tener instalado la librería rxjs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agregando algo de HTML y CSS
&lt;/h3&gt;

&lt;p&gt;No voy a dedicarle mucho tiempo al css ni html por no ser el punto central del post, ustedes pueden agregarle los estilos y dejarlo bien chido. En este caso solo agregaré un contenedor en el HTML&lt;br&gt;
&lt;iframe src="https://jsfiddle.net/jestea/v582hg6e/3//embedded/html,css//dark" width="100%" height="600"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Almacenar nodo contenedor e importar Rxjs
&lt;/h3&gt;

&lt;p&gt;Lo primero que haremos es importar la librería Rxjs y almacenar el nodo contenedor. Nada díficil verdad.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const Observable = Rx.Observable;
const container = document.getElementById('container');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ahora si viene lo bueno, el paso a paso.
&lt;/h3&gt;

&lt;p&gt;Lo que nos interesa a nosotros es el deslizamiento que hace el usuario al scrollear así que necesitamos escuchar ese evento, scroll. Con rxjs es bastante simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observable
  .fromEvent(container, 'scroll')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Genial, ahora es momento de "pensar" y decidir que valores necesitamos para consumir el servicio cada vez que el usuario scrollee. Para ello existen dos criterios.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El servicio debe ser consumido solo si el usuario se ha deslizado hacia abajo. Es decir, la posición actual debe ser mayor a la posición anterior. Genial&lt;/li&gt;
&lt;li&gt;Ahora, no podemos consumir el servicio hasta que llegue a un punto determinado, un límite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Para lograr estos criterios necesitamos tres propiedades que se encuentran en el objeto que el evento scroll nos retorna. &lt;code&gt;clientHeight, scrollHeight, scrollTop&lt;/code&gt;.&lt;br&gt;
Así que brevemente describiré que valor representa cada una de estas propiedades.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;clientHeight&lt;/strong&gt;: Altura del contenedor sin incluir la parte scrolleable. Altura inicial (fijo). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scrollTop&lt;/strong&gt;: Posición de la barra en el eje Y.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scrollHeight&lt;/strong&gt;: Altura total del contenedor incluyendo la parte scrolleable. Dinámica a medida que aumenta elementos hijos.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observable
  .fromEvent(container, 'scroll')
  .map(e =&amp;gt; ({
    scrollTop: e.target.scrollTop,
    scrollHeight: e.target.scrollHeight,
    clientHeight: e.target.clientHeight
  }))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Perfecto, ¿Para que nos sirve cada propiedad?&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Matemáticas
&lt;/h4&gt;

&lt;p&gt;La diferencia entre la posición actual y anterior nos brindará información si el usuario se deslizo hacía abajo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function isScrollDown(beforePosition, currentPosition) {
  beforePosition.scrollTop &amp;lt; currentPosition.scrollTop;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mientras que la razón entre la posición de la barra y la diferencia de alturas (scrollHeight y clienteHeight) nos dirá si ha pasado el &lt;em&gt;límite&lt;/em&gt; (nosotros definiremos el límite).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function setThreshold(threshold) {
  return function hasPassedThreshold(currentPosition) {
    return currentPosition.scrollTop * 100 /
      (currentPosition.scrollHeight -
       currentPosition.clientHeight) &amp;gt; threshold;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con las dos criterios que definimos podemos empezar a filtrar las posiciones que nos interesa.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observable
  .fromEvent(container, 'scroll')
  .map(e =&amp;gt; ({
    scrollTop: e.target.scrollTop,
    scrollHeight: e.target.scrollHeight,
    clientHeight: e.target.clientHeight
  }))
  .pairwise() // emite el valor anterior y el actual en un array. 
  .filter(positions =&amp;gt; isScrollDown(positions[0], positions[1]) &amp;amp;&amp;amp; 
  setThreshold(80)(positions[1]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Loader
&lt;/h3&gt;

&lt;p&gt;Agregue un loader simple al final del container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const toogleLoading = (function (container) {
  const loading = document.createElement('p');
  loading.classList.add('bold', 'text-center');
  loading.innerText = 'Loading...';
  return function toogleLoading(showLoader) {
  showLoader ? container.appendChild(loading) : loading.remove();
}
})(container);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora, mostramos el loader cada vez la posición del scrollbar retorne true según los criterios establecidos. Para ello usamos el operador do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observable
  .fromEvent(container, 'scroll')
  .takeWhile(res =&amp;gt; nextUrl)
  .map(e =&amp;gt; ({
    scrollTop: e.target.scrollTop,
    scrollHeight: e.target.scrollHeight,
    clientHeight: e.target.clientHeight
  }))
  .pairwise()
  .filter(positions =&amp;gt; isScrollDown(positions[0], positions[1]) &amp;amp;&amp;amp; setThreshold(80)(positions[1]))
  .do(() =&amp;gt; toogleLoading(true)) // show loader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Consumiendo el servicio
&lt;/h3&gt;

&lt;p&gt;El consumo del servicio debe ir acompañado de la visualización del loader. A lo que voy es que un servicio puede ser rápido o bastante lento. Por el lado front debemos mostrar al usuario que efectivamente se esta cargando datos, y eso lo hacemos a través de un loader. Sin embargo, cuando la respuesta del servicio es rápida el loader se muestra solo un instante y no se ve nada bien. Para mayor información encontré este gran &lt;a href="https://codeburst.io/rxjs-show-spinner-for-a-minimum-amount-of-time-807ac6b23227" rel="noopener noreferrer"&gt;post&lt;/a&gt; que trata sobre como agregar un loader con un tiempo mínimo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observable
  .fromEvent(container, 'scroll')
  .takeWhile(res =&amp;gt; nextUrl)
  .map(e =&amp;gt; ({
    scrollTop: e.target.scrollTop,
    scrollHeight: e.target.scrollHeight,
    clientHeight: e.target.clientHeight
  }))
  .pairwise()
  .filter(positions =&amp;gt; isScrollDown(positions[0], positions[1]) &amp;amp;&amp;amp; setThreshold(80)(positions[1]))
  .do(() =&amp;gt; toogleLoading(true)) // show loader
  .switchMap(() =&amp;gt; Observable.combineLatest(Observable.timer(1000), Observable.ajax({
    url: nextUrl,
    method: 'GET'
  })))
  .map(combine =&amp;gt; combine[1])
  .catch(console.error)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Más lento cerebrito
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;switchMap&lt;/strong&gt;, nos permite subscribirnos a los nuevos observables que son emitidos desde el inner observable (en este caso el combineLatest). Cuando llega un nuevo observable el anterior es cancelado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;combineLatest&lt;/strong&gt;, emite el ultimo valor de cada uno de los observables. Los valores emitidos por cada observable son almacenados en un arreglo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;timer&lt;/strong&gt;, emite numeros en secuencia según el tiempo indicado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ajax&lt;/strong&gt;, crea un ajax request siguiendo el concepto de los observables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;map&lt;/strong&gt;, convierte cada valor emitido según el project function que se pasa como parametro&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;catch&lt;/strong&gt;, maneja los posibles errores que puedan darse&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Manejando el response
&lt;/h4&gt;

&lt;p&gt;Usamos el operador &lt;strong&gt;do&lt;/strong&gt; en caso que quisieramos ejecutar un &lt;em&gt;side effect&lt;/em&gt; (cambiar el valor de alguna variable o ejecutar alguna función). El response del servicio nos devuelve un objeto extenso el cual contiene la siguiente url a consultar además de un arreglo con todos los pokemones. En este caso utilizamos el operador &lt;strong&gt;do&lt;/strong&gt; para actualizar nuestro endpoint. Por otro lado, utilizamos el operador &lt;strong&gt;map&lt;/strong&gt; para solo obtener la propiedad &lt;em&gt;results&lt;/em&gt; del objeto response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observable
  .fromEvent(container, 'scroll')
  .takeWhile(res =&amp;gt; nextUrl)
  .map(e =&amp;gt; ({
    scrollTop: e.target.scrollTop,
    scrollHeight: e.target.scrollHeight,
    clientHeight: e.target.clientHeight
  }))
  .pairwise()
  .filter(positions =&amp;gt; isScrollDown(positions[0], positions[1]) &amp;amp;&amp;amp; setThreshold(80)(positions[1]))
  .do(() =&amp;gt; toogleLoading(true)) // show loader
  .switchMap(() =&amp;gt; Observable.combineLatest(Observable.timer(1000), Observable.ajax({
    url: nextUrl,
    method: 'GET'
  })))
  .map(combine =&amp;gt; combine[1])
  .catch(console.error)
  .do(res =&amp;gt; (nextUrl = res.response.next))
  .map(res =&amp;gt; res.response.results)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Suscribirnos
&lt;/h3&gt;

&lt;p&gt;Finalmente tenemos que suscribirnos a nuestro observable scroll. Y en nuestro caso de &lt;em&gt;success&lt;/em&gt; debemos de dejar de mostrar el loading así como agregar todos los pokemones en nuestro container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observable
  .fromEvent(container, 'scroll')
  .takeWhile(res =&amp;gt; nextUrl)
  .map(e =&amp;gt; ({
    scrollTop: e.target.scrollTop,
    scrollHeight: e.target.scrollHeight,
    clientHeight: e.target.clientHeight
  }))
  .pairwise()
  .filter(positions =&amp;gt; isScrollDown(positions[0], positions[1]) &amp;amp;&amp;amp; setThreshold(80)(positions[1]))
  .do(() =&amp;gt; toogleLoading(true)) // show loader
  .switchMap(() =&amp;gt; Observable.combineLatest(Observable.timer(1000), Observable.ajax({
    url: nextUrl,
    method: 'GET'
  })))
  .map(combine =&amp;gt; combine[1])
  .catch(console.error)
  .do(res =&amp;gt; (nextUrl = res.response.next))
  .map(res =&amp;gt; res.response.results)
  .subscribe(pokemons =&amp;gt; {
    toogleLoading(false);
    container.innerHTML += pokemons.map(pokemon =&amp;gt;
                                                pokemon.name).join('&amp;lt;br&amp;gt;')
  });

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Código completo
&lt;/h3&gt;

&lt;p&gt;Cualquier duda, pregunta o feedback pueden dejar sus comentarios. No se olviden de &lt;strong&gt;aprender&lt;/strong&gt; y &lt;strong&gt;compartir&lt;/strong&gt; ❤️. Hasta la próxima.&lt;br&gt;
&lt;iframe src="https://jsfiddle.net/jestea/v582hg6e/4//embedded//dark" width="100%" height="600"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>rxjs</category>
      <category>javascript</category>
      <category>angular</category>
    </item>
  </channel>
</rss>
