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" />
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));
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},
... ]
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) )
});
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);
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) );
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');
});
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>
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.
Top comments (1)
Is mergeMap equal switchMap with takeuntil, while user typing?
I always use switchMap with takeUntil, is this alright?