Predictive Search – Programación Reactiva con RXJS

La programación reactiva es un paradigma de programación basado en flujos de datos (streams) y, concretamente, en la propagación de los cambios a través de dicho flujo. 

ReactiveX (RXJS) es una librería que nos permite gestionar este flujo de datos de forma asíncrona mediante el manejo de eventos. Esta librería aglutina las mejores ideas del patrón Observer, el patrón Iterator y la Programación Funcional.

Antes de comenzar, me gustaría aclarar que no soy ningún experto en RXJS. Lo que pretendo con este artículo es mostrar un ejemplo sencillo, basado en mi experiencia personal, de lo que puede ofrecer RXJS (¡y lo que mola! :D) con el fin de que a alguien le sirva de utilidad. Dicho esto, ¡al lío!

Para aplicar estos conceptos vamos a ver en detalle el código utilizado para realizar un buscador predictivo utilizando streams de datos. Todo el código está disponible en este repositorio donde lo puedes compilar de forma local para hacer tus pruebas:

Predictive Search

El servicio utilizado para mostrar los datos consiste en un servidor local que devuelve una listado de los países del mundo. Para ello se ha utilizando json-server el cual nos permite crear una API REST bastante completa en muy poco tiempo. En nuestro caso para realizar las llamadas se utiliza un parámetro “q” con el String a buscar y el servicio nos devuelve las coincidencias de dicho String en todo el array de países.

A continuación se explica el código y los operadores de RXJS utilizados para realizar la búsqueda predictiva. Comenzamos con el HTML, el elemento input es el que contiene el código manejado a través de streams de datos:

<input class="predictive-input"

        [placeholder]="placeholder"

        (keyup)="searchPredictive($event.target.value)"

        [(ngModel)]="query"

        #queryModel="ngModel">

En este caso, nos interesa centrarnos en el evento “keyup”. Si vemos que llama a la función «searchPredictive» con parámetro el valor introducido por teclado. Bien, ahora vamos al código Typescript:

public searchPredictive(terms: string): void {

    if (this.predictive && this.selectedItem) {

      this.reset.emit(null);

      this.queryUsed = null;

      this.selectedItem = null;

    }

    if (!this.predictive && this.searchError) {

      this.searchError = false;

    }

    terms = terms.trim();

    this.inputString.next(terms);

  }

En esta función se realizan varias comprobaciones ya que el buscador admite dos tipos de comportamiento:

1- Escribir y pulsar el botón «Buscar»

2- Mostrar resultados a medida que se va escribiendo (que es el objeto del artículo)

Si bien observamos, después de las comprobaciones se hace una llamada a «this.inputString.next(terms);» con los caracteres que llevamos introducidos en el input. En este punto se encuentra la magia, ya que InputString es un

  private inputString = new Subject<any>();
Que al realizar su llamada a «next» desencadena el proceso que se explica a continuación. Este proceso se desencadena ya que en el constructor del componente nos suscribimos a los cambios originados en nuestro «inputString«.
this.inputString

      .debounceTime(this.debounceTime)

      .distinctUntilChanged()

      .filter(query => query && query.length >= this.minLength ? true : this.listResults = null)

      .filter(query => {

        if (!this.queryUsed || query.length <= this.queryUsed.length || this.queryUsed.results > 0) { return true; }

        if (query.length > this.queryUsed.length && this.queryUsed.results < 1) { return false; }

      })

      .switchMap(terms => {

        const params = new HttpParams({

          fromString: `${this.queryParamName}=${terms}`

        });

        params.append(this.queryParamName, terms);

        return this.webSearchService

          .search(params)

          .map(

          results => {

            this.results = results;

            this.listResults = this.normalizeResults(results, terms);

            this.queryUsed = { length: terms.length, results: results.length };

          }

          .catch(error => {

            this.searchError = true;

            this.results = null;

            this.listResults = null;

            return Observable.of(error);

          });

      })

      .subscribe();
Veamos por partes los operadores de RXJS utilizados en esta función:
  • debounceTime: Descarta todos los valores emitidos entre el tiempo especificado. Es decir, en nuestro caso, previene llamadas al servicio innecesarias mientras el usuario esta escribiendo.
  • distinctUntilChanged: Solo continúa cuando el valor recibido es diferente al anterior. En este caso utilizando este operador logramos evitar realizar la misma búsqueda más de una vez.
  • filter: Emite los valores que cumplan una determinada condición. En este ejemplo hemos utilizado dos funciones de filtrado. La primera comprueba la longitud mínima de la cadena para realizar la búsqueda. Con la segunda evitamos la llamada al servicio si la cadena anterior no ha devuelto resultados. Por ejemplo, si realizando la búsqueda con la cadena «Esap» no obtenemos ninguna coincidencia en nuestros datos, cualquier carácter que añadamos a continuación tampoco lo hará.
  • switchMap: Este operador se encarga de cancelar cualquier evento previo que no haya finalizado obteniendo el ultimo evento emitido. En este caso nos sirve para realizar la búsqueda del String completo introducido. Si, por ejemplo, el usuario introduce «Pozuelo» y acto seguido sigue escribiendo «Pozuelo de«, llegarían dos eventos que mediante el uso de switchMap conseguimos tratar la última cadena ya que los eventos lanzados anteriormente que no han finalizado se han cancelado.
  • map: Trata cada valor recibido. Distribuye los datos del observable para poder tratarlos de forma individual. A diferencia del map original, este operador no acumula los datos en estructuras intermedias, si no que directamente devuelve un observable para poder utilizarlo con el siguiente operador (en caso de haberlo).
  • subscribe: Realiza notificaciones sobre las emisiones de un observable. De esta manera, todos los cambios originados en nuestro input, pasarán por todo el proceso descrito anteriormente.

Así pues, los resultados se almacenan en nuestra variable listResults y Angular se encarga de mostrar todos los resultados mediante un bucle en nuestro template:

<ul *ngIf="listResults" class="predictive-search__results" [class.predicitive]="predictive">

  <li class="predictive-search__results-item" *ngFor="let result of listResults; let i = index" (click)="selectItem(i)">

    <p [innerHTML]="result"></p>

  </li>

</ul>

Eso es todo, espero que os pueda servir de ayuda o como mini-guía para comenzar con RXJS. Estaré muy agradecido de recibir cualquier comentario, duda, sugerencia y/o mejora.

 

Referencias:

https://twitter.com/pablo_magaz

https://pablomagaz.com/blog/como-funcionan-operadores-rxjs

https://pablomagaz.com/blog/programacion-reactiva-con-rxjs

https://medium.com/@juliapassynkova

https://martinfowler.com/articles/collection-pipeline/

https://rxmarbles.com/

<span style="font-size:80%">Autor </span><a href="https://blog.kairosds.com/author/carlos-azanon-caceres/" target="_self">Carlos Azanon Caceres</a>

Autor Carlos Azanon Caceres

Feb 14, 2018

Otros artículos

Anotación @Lazy

Anotación @Lazy

¿Qué es? Es una anotación de Spring que nos permite posponer la creación de beans, de tal forma que éstos sólo se crearán cuando se vayan a utilizar, en lugar de crearlos al iniciar la aplicación. Ésto nos puede servir en aplicaciones que tienen funcionalidades muy...

Deceye – Turning transparency into depth

Deceye – Turning transparency into depth

¿DeFi? ¿DAOs? ¿En qué consiste eso? ¿Qué papel juegan? El ecosistema DeFi, Decentralized Finances, en castellano Finanzas Descentralizadas, engloba a todos aquellos servicios financieros que gracias a la tecnología blockchain pueden evitar la intermediación que ofrece...