RxJS in practice: how to make a typeahead with streams!

sammyisa

Sammy Israwi

Posted on August 19, 2017

RxJS in practice: how to make a typeahead with streams!

What we will do

You know when you type into the Twitter search bar and it tries to guess what you want to search for? Say you start typing "SammyI" and the first result is my twitter handle, @SammyIs_Me.

That's what we'll make (except probably maybe not for Twitter).

But first, housekeeping

Last time we talked about streams and actions we do on those streams, but I did not use the correct terminology there. The streams are called Observables, and I will refer to those as such in the future.

Starting code

Let's skip the basic HTML, just make something like so:

<input type="text" 
       name="typeahead" 
       class="typeaheadInput" />
Enter fullscreen mode Exit fullscreen mode

You can add some CSS to make it look nicer if you want. Next, the starting Javascript - a stream observable that sends new data at every change of the input text box, and a function that logs the input to the console:

const inputStream$ = Rx.Observable
        .fromEvent(input, 'input')
        .map(e => e.target.value);

inputStream$.subscribe(text => console.log(text));
Enter fullscreen mode Exit fullscreen mode

We are even 'sanitizing' the observable to only get the useful data from the event.

Getting the search data

To get the search/suggestion data, we will use Datamuse API. We will use the suggestion endpoint to get some word suggestions, like so:

GET https://api.datamuse.com/sug?s=sammy
Response:
[{"word":"sammy","score":35841},
{"word":"sammy sosa","score":35639}, 
... ]
Enter fullscreen mode Exit fullscreen mode

Let's add that the request to our subscribe of our observable, and we have:

inputStream$.subscribe(text => {
    fetch(`https://api.datamuse.com/sug?s=${text}`)
    .then( resp => resp.json() )
    .then( resp => console.log(resp) )
});
Enter fullscreen mode Exit fullscreen mode

Now we are showing to the console an array of all the suggestions from the API. We are not done, but you can see the final product from here!

Making the search data also an observable

We continuously getting a stream of data from datamuse, can't we just make that another stream to be consumed? Yes we can!

There are a few new, important concepts in this section to tackle, so make sure you get a good grasp on it before moving on.

First, we don't want to hit the datamuse endpoint at every single stroke. If we do, we will be getting recommendations for h, he, hel, hell, hello and we only need the recommendations for the hello.

So, we will debounce the observable. Debouncing means 'wait until we haven't received a new event on the stream for x milliseconds, then get the latest item and that's the new item of the observable. So, in our example from before, after we stop typing for one second, only hello will be sent to the observable. Try it out, change the inputStream$ observable from before:

const inputStream$ = Rx.Observable
        .fromEvent(input, 'input')
        .map(e => e.target.value)
        .debounceTime(2000);
Enter fullscreen mode Exit fullscreen mode

Type on the input box and then wait for two seconds. Keep an eye on the console.

Let's make the search results a new observable!

const suggestionsStream$ = inputStream$
    //Fetch the data
    .mergeMap( text => fetch(`https://api.datamuse.com/sug?s=${text}`) )
    //Get the response body as a json
    .mergeMap( resp => resp.json() )
    //Get the data we want from the body
    .map( wordList => wordList.map(item => item.word) );
Enter fullscreen mode Exit fullscreen mode

I promise I will get into mergeMap soon, but first I must ask you to just trust in it. If you are dealing with a promises, use mergeMap instead of map
Now that we have an observable that gives us an array of suggestions, we put that array somewhere.

Since this is getting a bit longer than I anticipated, we will just list the suggestions in a div somewhere:

//We made a div of class 'suggestion' for this
const suggestions = document.querySelector('.suggestions');
suggestionsStream$.subscribe(words => {
    suggestions.innerText = words.join('\n');
});
Enter fullscreen mode Exit fullscreen mode

Now try it out! Type something, wait two seconds, look at the results!

Final Code

<input type="text" 
    name="typeahead" 
    class="typeaheadInput" />
<div class="suggestions"></div>
<script>
//Get the suggestions div
const suggestions = document.querySelector('.suggestions');
//Get the input component
const input = document.querySelector('.typeaheadInput');

//Input stream
const inputStream$ = Rx.Observable
                .fromEvent(input, 'input')
                .map(e => e.target.value)
                .debounceTime(2000);

//Suggestions stream
const suggestionsStream$ = inputStream$
                .mergeMap( text => fetch(`https://api.datamuse.com/sug?s=${text}`) )
                .mergeMap( resp => resp.json() )
                .map( body => body.map(item => item.word) )

//Handle the stream
suggestionsStream$.subscribe(words => {
    suggestions.innerText = words.join('\n');
});
</script>
Enter fullscreen mode Exit fullscreen mode

Next time we will explain what mergeMap is (probably a shorter one, it is so much more than a promise handler!) and we will dive into animations with RxJS! If you have any questions/corrections/suggestions/compliments you can reach me via Twitter @SammyIs_Me.

💖 💪 🙅 🚩
sammyisa
Sammy Israwi

Posted on August 19, 2017

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related