JavaScript30 - Day 6 (Type Ahead - Autocomplete)

This is day 6 in my #javascript30 journey. This is the free course from Wes Bos that lets you brush up on your JavaScript skills via 30 projects.

Yesterday we created an awesome Image Gallery using Flexbox. You can keep track of all the projects we're building here.

Today we're creating an auto-filling search bar using Ajax Type Ahead.

This is what we're starting with:

Starting HTML


Day 6 - Ajax Type Ahead

Making websites intuitive and easy to use is one of the great tasks in web development. Part of this is predicting what the user wants and providing this to them quickly. This is where today's exercise can be so powerful.

We will be creating a text box that searches the name and population of American cities. We are gathering this data from an external JSON. The data structure looks like this:

{
    "city": "New York", 
    "growth_from_2000_to_2013": "4.8%", 
    "latitude": 40.7127837, 
    "longitude": -74.0059413, 
    "population": "8405837", 
    "rank": "1", 
    "state": "New York"
}

Our search will be over the city and state attributes in this dataset.

Accessing the data

Now that we know what the end goal is the first step is to access the data. To do that we can use the fetch API. Fetch returns a promise. This means it will return a response whether the fetch was successful or not.

We can wait until the promise is resolved by calling the then method. In this example we don't handle any failure cases of the Promise.

Once the response is returned we need to convert it from the raw data that is provided back into JSON. We can do this by using the json method that is returned on the data blob. This will return the second promise that contains the JSON data.

Once we have the JSON data we can use the ES6 spread to push the new array into our cities variable.

This all boils down to:

const endpoint =  
  'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json'

const cities = []

fetch(endpoint)  
  .then(blob => blob.json())
  .then(data => cities.push(data))
Find Matches

Now that we have all of the data that we care about in a format that we can read we need to add an event listener to the search bar. This will filter the massive array into a subset that only contains the results that include a specific string.

To do this we will use the filter method along with some regex fun. We need to put a variable (the user input) into the regex along with the global and insensitive flags. We need to check whether our regex matches the values in both city and state. This boils down to:

function findMatches(wordToMatch, cities) {  
  return cities.filter(place => {
    //Is the city or state a match for the search?
    const regex = new RegExp(wordToMatch, 'gi')
    return place.city.match(regex) || place.state.match(regex)
  })
}
Display Matches

Now that our search and filter method is working we need a way to display the results to the user.

This will need to be called and updated each time the user updates the search field or the two options below it (filter by city or state).

We will be listening for both the change and keyup events on the search bar.

Once the changes have been registered we can call the findMatches function that we already created to do the filtering. We will map the resulting Array into some JSX to display the values that we care about (City, State and Population).

This boils down to:

function displayMatches() {  
  const matchArray = findMatches(this.value, cities);
  const html = matchArray.map(place => {
    return `
      <li>
        <span class="name">${place.city}, ${place.state}</span>
        <span class="population">${place.population}</span>
      </li>
    `
  }).join('')
  suggestions.innerHTML = html;
}

const searchInput = document.querySelector('.search')  
const suggestions = document.querySelector('.suggestions')

searchInput.addEventListener('change', displayMatches)  
searchInput.addEventListener('keyup', displayMatches)  

The search function is now working:

Search function working

Highlighting the Search String

It is a nice feature to highlight the search string as it appears in the results. For instance, if we search for bos then when this string appears in the results it should be highlighted.

This can be done with regex and then replacing the result with JSX:

//Find the user input & highlight it on the results
const regex = new RegExp(this.value, 'gi')  
const cityName = place.city.replace(  
  regex,
  `<span class='hl'>${this.value}</span>`,
)
const stateName = place.state.replace(  
  regex,
  `<span class='hl'>${this.value}</span>`,
)

This is a subtle addition but increases the intuitive feel of the feature:

Search working with highlighting

Format Population Numbers

Large numbers are easier to read when they are separated by commas. Again, this is a small feature that improves the usability and intuitiveness of the design.

To format the population with a comma we need to create a new function:

function numberWithCommas(x) {  
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

We can then call that when adding the variable into the JSX:

<span class="population">${numberWithCommas(place.population)}</span>  

Now that everything has been done I have pushed this project live to a Firebase project. You can view it live here.

You can keep track of all the projects in this JavaScript30 challenge here.