RxJS - Auto Complete

5 years ago

RxJS makes it easy to set up an Auto Complete and provides some excellent functional programming patterns for doing so. In this example, we are querying a REST API that returns countries given a partial name. I have posted some code below, so let's break it down.

  • init - just an entry point that takes the id of an input element to wire everything up.
  • searchByValue - a simple Observable using the Fetch API that takes a value and passes it as a query string to our REST API. This method can replaced with anything that returns an Observable and we aren't locked into using fetch or REST. (See my blog post here: RxJS - Fetch)
  • obs = Rx.Observable.fromEvent - the heart of how the Auto-Complete functionality works is broken down here:
    • fromEvent - create our observable with an element and an event. We could use and element and any event, but in this case we only care about an input elements.
    • map - the event emits the element itself (e), so we need to return the value. If this were a checkbox, we would use checked.
    • filter - since we may be dealing with huge datasets, filter allows us to discontinue for any text with a length less than 3.
    • debounceTime - allows us to set a threshold for changes that are performed when typing. For example, if a user types 'abc', but immediately changes to 'abd', we don't want to make a network request on that first value. This may seem trivial, but can help improve performance and overall health of the system.
    • distinctUntilChanged - simply prevents continuation if the value is the same.
    • switchMap - final step simply projects our value to our searchByValue method.

As you can see, setting up an Auto-Complete in RxJS is not only surprisingly easy, and given how few lines of code we have, it is very readable and easy to understand what is going on.

JavaScript (autoComplete.js)

const sample = {
  init: inputId => {
    const baseUrl = 'https://dev-rest-api.herokuapp.com/countries'
    const fetchConfig = { method: 'GET' }
    const minCharacters = 3

    // Observable from fetch promise
    const searchByValue = value =>
      Rx.Observable.fromPromise(fetch(baseUrl + '?country_like=' + value, fetchConfig)).switchMap(response => {
        if (response.ok) {
          return Rx.Observable.fromPromise(response.json())
        } else {
          return Rx.Observable.throw(response)
        }
      })

    // Observable from keyup event
    const obs = Rx.Observable.fromEvent(document.getElementById(inputId), 'keyup')
      .map(e => e.target.value)
      .filter(text => text.length >= minCharacters)
      .debounceTime(750)
      .distinctUntilChanged()
      .switchMap(searchByValue)

    // Subscribe and print
    obs.subscribe(
      results => utility.updateResults('Next', JSON.stringify(results, null, 2)),
      error => utility.updateResults('Error', error.status + ' - ' + error.statusText),
      () => utility.updateResults('Complete')
    )
  }
}
const utility = {
  updateResults = (title, results) => {
    console.group()
    console.log(title)
    if(results) {
      console.log(results)
    }
    console.groupEnd()
  }
}

HTML (autoComplete.html)

<input id="auto-complete" type="text" class="form-control" placeholder="Search for Country (minimum 3 characters)" />
<script>
  sample.init('auto-complete')
</script>

CDN Reference

<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.3/Rx.min.js"
  integrity="sha256-hRKdKxNWF3kA5HoYA7GoSRILnmbQS4cwv23bJwqJlns=" crossorigin="anonymous"></script>

Screenshot



Discuss on Twitter