Auto-suggest feature using RxJs and Vanilla Javascript

Implementing search as you type feature with RxJs

In this post, I will be showing you a way to implement an auto-suggest feature using RxJs and vanilla Javascript. You can use this in an application written in angular or any other front-end framework.

Although I will be explaining the components used in this project,
to better understand this article you will need to have some familiarity with RxJs.

Project Structure:
index.html
style.css
script.js
util.js

Let’s start with util.js as it is the simplest file to understand. The file contains the following components

util.js

1. searchItems: To keep things single we are not connecting to an actual backend API, instead we are using in-memory data.
2. render: It is a function that takes a container (basically a place to attach HTML to) and attaches a template (HTML) to it.
In the render function we are doing the following things:
a. Fetching a template define in our index.html, remember template is nothing but the content to be rendered
b. Taking the container (The position it the index.html where the template should be rendered) and attaching the template to the container.
3. getResult: It is a function that performs a filter on the searchItems array to return data based on input data. Again this function
is there to represent a backend API that would do this for us.

Below you can see util.js file

const util = {
   searchItems : ['Peter', 'Chris', 'Christine', 'Paul', 'Lois', 'Lauren'],
   render: container => {
    let template = document.getElementById('search-auto');
    let div = template.content.cloneNode(true);
    container.appendChild(div);
  },
  getResult(searchItem) {
    if (searchItem === '') {
      return [''];
    }
    var res = util.searchItems.filter(item => item.startsWith(searchItem));
    if (res.length == 0) {
      return ['No Data Found'];
    }
    return res;
  }
}

script.js

We are calling the render method discussed above from util.js. We are passing an HTML container that is defined in index.html.
This tells Javascript where the template should be rendered.

Note: The use of a template is completely optional and you can just use your HTML inline in the index.html.

Below you can find a script.js file

document.addEventListener('DOMContentLoaded', () => {
  let main = document.querySelector('main');
  util.render(main);
});

Now that we have covered the boilerplate stuff, let us focus on how we use RxJs to implement Auto-Suggest feature

     Rx.Observable.fromEvent(document.getElementById('obs'), 'input')
            .map((e) => e.target.value)
            .debounceTime(350)
            .distinctUntilChanged()
            .switchMap((val) => Rx.Observable.of(val))
            .subscribe((text) => {
                ...
            });

Now let’s break down what is happening in the above block of code.

  • First, we create an Observable from the ‘input’ event. This Observable will emit a value every time user types something in the input field.
  • We call the map on the Observable, This will take the event emitted and return us the input entered by the user.
  • We use the debounceTime() operator from RxJs. debounceTime is a lossy technique to deal with backpressure (data is being produced faster than it is being consumed).debounceTime will take input from the user but will only emit values produced every 350ms. This way we do not end up calling a backend service every time a user inputs something
  • We use the distinctUntilChanged operator, this operator only emits the current value if it is different from the previous value.
  • Then we use switchMap, this operator rejects all other previous emitted values and takes into account only the currently active value. This way we can deal with multiple responses being returned from the backend API.
  • Next, we subscribe to the Observable and pass a callback function, In the callback function we receive the response from the backend API (In our case util.js) and we use that data to render the response on HTML.

index.html

<!-- Search With 'Peter', 'Chris', 'Christine', 'Paul', 'Lois', 'Lauren' for Response -->

<head>
  <script src="https://npmcdn.com/@reactivex/rxjs@5.0.0-beta.6/dist/global/Rx.umd.js"></script>
  <script src="util.js"></script>
  <script src="main.js" type="module"></script>
</head>

<div>
  <img src="https://playcode.io/static/img/logo.png" 
       alt="PlayCode logo">
  <h1></h1>
  <input type="text" id="obs" placeholder="Search As you Type" autocomplete="off"/>
  <main>
  </main>
  <template id="search-auto">
    <div class="search-container no-data"><ul></ul></div>
  </template>

  <script>
    let df = document.createDocumentFragment();

    Rx.Observable.fromEvent(document.getElementById('obs'), 'input')
      .map((e) => e.target.value)
      .debounceTime(350)
      .distinctUntilChanged()
      .switchMap((val) => Rx.Observable.of(val))
      .subscribe((text) => {
        console.log('Event Fired for ', text)
        let ul = document.querySelector('.search-container ul');
        ul.innerHTML = '';
        result = util.getResult(text);
         let ulClass = document.querySelector('.search-container');
         let classes = ulClass.classList;
        if (result.length === 1 && result[0] == 'No Data Found') {
          classes.add("no-data")
        } else {        
          classes.remove("no-data")
        }
        result.forEach(res => {
          let li = document.createElement('li');
          li.innerText = res;
          df.appendChild(li);
        });
        ul.appendChild(df);
      });
  </script>

</div>

style.css

body {
  background: white;
  color: #323232;
  font-weight: 300;
  height: 100vh;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: Helvetica neue, roboto;
}

img {
  width: 56px;
  height: 48px;
}

h1 {
  font-weight: 200;
  font-style: 26px;
  margin: 10px;
}

main {
  background-color: #eee;
  grid-area: lists;
  display: flex;
  flex-direction: row;
  justify-content: space-evenly;
  align-items: stretch;
}

.search-container {
  padding: 0rem;
  margin: 0rem;
  border: 1px solid #ddd;
  flex-basis: 100%;
  background-color: hsl(200deg, 50%, 80%);
  color: hsl(200deg, 50%, 20%);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
    Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  font-weight: 500;
  text-shadow: 2px 2px 4px #999;
  font-size: 1rem;
}

.no-data {
  color: red !important;
  background-color: white !important;
}

.search-container ul{
  list-style-type: none;
  text-align: center;
  margin: 0 auto;
  margin-left: -40px;
}

You can find the source code here.

Leave a Comment

Your email address will not be published. Required fields are marked *

%d bloggers like this: