DEV Community

Cover image for Smart input box with auto-suggestions for the users (two methods)
Indika Rajapaksha
Indika Rajapaksha

Posted on • Edited on

Smart input box with auto-suggestions for the users (two methods)

Almost every internet user is using search engines (Google, Bing, DuckDuckGo etc.) on daily basis to browse internet, and you should have seen that search engines are smart enough to help us on suggesting possible search keyword or phrases when we start typing in the search box. The mechanism behind getting phrases or keywords could be complicated and hidden in the servers; but the concept used for search box suggestions simple enough to understand and implement easily. Today, I'm going to show how to implement a simple input box with auto suggestion feature. There won't be any back-end code involved: we only writing front-end JavaScript code while simulating the back-end behavior.

There are two ways we can implement this: I'll cover both ways in this article.

1 Using a datalist element available in HTML 5 standard, which was created solely for this purpose. However, this method is somewhat less preferred due to following reasons.

  • Suggestions are appearing similar to how web browsers show saved logins and recently typed entries. Hence, user may think it comes from the browser history not from front-end code.

  • Listing of suggestions use option tag, which is hard to style with pure CSS due to the restrictions put the tag.

2 Create a separate div element which appears below the input box. This method needs more coding and ahead of thinking to populate: there are several events to be handled.

Note: In this example, we simply use a list of countries for the demonstration purpose: implementations DO NOT restrict user from modifying the value in the input box. Hence, if you need exact value to be send for the back-end, select box is the best choice.

At the basic level there are only little differences in two methods: the HTML element we use to display data and how we populate the suggestions. Therefore, we can reuse most of our JavaScript code in both methods.

Let's start with common code that will not change. I use vanilla JavaScript to make this sense for wide range of developers (so you don't need to learn any third party libraries).

First create a simple HTML template linked to CSS (styles.css) and JavaScript (main.js) files.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Auto Suggest Text</title>
    <link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
    <div id="content">
        <h1>Auto Suggest Text</h1>
        <br>

        <label for="country">Country:</label>
        <div style="margin-top: 5px;">
            <input type="text" name="country" id="country">
        </div>
    </div>

    <script type="text/javascript" src="main.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Here is the common CSS file for both implementations:

body {
    margin: 0;
    font-family: arial;
}

#content {
    width: 80%;
    margin: 0 auto;
}

label {
    font-size: 1.2em;
    color: gray;
}

input {
    width: 30%;
    background-color: white;
    padding: 5px;
    border: 1px solid lightgray;
    border-radius: 5px;
    font-size: 1.2em;
}

/* for the second method: div */
#countries {
    border: 1px solid lightgray;
    width: 30.5%;
    padding: 1px;
    border-collapse: true;
}

.item {
    padding: 10px 5px;
    margin: 0;
}

.item:hover {
    background-color: royalblue;
    color: white;
}

.hide {
    display: none;
}
Enter fullscreen mode Exit fullscreen mode

Significant amount of JavaScript code re-used in both methods. Following is the common code along with brief description of the logic.

let inputBoxOldVal = '' // holds the previous value of input box
let inputBox = null
Enter fullscreen mode Exit fullscreen mode

I use inputBoxOldVal to store the previous value typed in the input box. This helps us to trigger the AJAX call only if input box value changed from the previous, which necessarily filter out any irrelevant key press events such as Caps Lock.

Note: Most developers are warned against using global variables. But this is not a huge issue in our code.

document.addEventListener('DOMContentLoaded', (e) => {
    inputBox = document.getElementById('country')

    inputBox.addEventListener('keyup', (e) => {
        let newVal = inputBox.value.trim()

        if ( newVal && newVal !== inputBoxOldVal ) {
            inputBoxOldVal = newVal
            clearSuggestions()
            getSuggestions(newVal, onMatchingData)
        } else if ( !newVal ) {
            inputBoxOldVal = ''
            clearSuggestions()
        }
    })
})
Enter fullscreen mode Exit fullscreen mode

We'll listen to the keyup event of the input box and trim its value to remove any leading/trailing spaces, that prevents unnecessary AJAX calls. Comparing newVal with inputBoxOldVal is straightforward. If the input box value has been changed, store it in the variable and clear any previous suggestions that were provided. Then make an AJAX request (getSuggestions) with the input value and pass a callback function (onMatchingData) to handle returned data.

// this will only simulate the AJAX call
const getSuggestions = (value, callback) => {
    const countries = [
        'Afghanistan','Albania','Algeria','American Samoa','Andorra','Angola','Anguilla',
        'Antigua & Barbuda','Argentina','Armenia','Aruba','Australia','Austria','Azerbaijan',
        'Bahamas, The','Bahrain','Bangladesh','Barbados','Belarus','Belgium','Belize','Benin',
        'Bermuda','Bhutan','Bolivia','Bosnia & Herzegovina','Botswana','Brazil','British Virgin Is.',
        'Brunei','Bulgaria','Burkina Faso','Burma','Burundi','Cambodia','Cameroon','Canada',
        'Cape Verde','Cayman Islands','Central African Rep.','Chad','Chile','China','Colombia',
        'Comoros','Congo, Dem. Rep.','Congo, Repub. of the','Cook Islands','Costa Rica',
        'Cote d\'Ivoire','Croatia','Cuba','Cyprus','Czech Republic','Denmark','Djibouti','Dominica',
        'Dominican Republic','East Timor','Ecuador','Egypt','El Salvador','Equatorial Guinea',
        'Eritrea','Estonia','Ethiopia','Faroe Islands','Fiji','Finland','France','French Guiana',
        'French Polynesia','Gabon','Gambia, The','Gaza Strip','Georgia','Germany','Ghana','Gibraltar',
        'Greece','Greenland','Grenada','Guadeloupe','Guam','Guatemala','Guernsey','Guinea',
        'Guinea-Bissau','Guyana','Haiti','Honduras','Hong Kong','Hungary','Iceland','India',
        'Indonesia','Iran','Iraq','Ireland','Isle of Man','Israel','Italy','Jamaica','Japan','Jersey',
        'Jordan','Kazakhstan','Kenya','Kiribati','Korea, North','Korea, South','Kuwait','Kyrgyzstan',
        'Laos','Latvia','Lebanon','Lesotho','Liberia','Libya','Liechtenstein','Lithuania','Luxembourg',
        'Macau','Macedonia','Madagascar','Malawi','Malaysia','Maldives','Mali','Malta',
        'Marshall Islands','Martinique','Mauritania','Mauritius','Mayotte','Mexico',
        'Micronesia, Fed. St.','Moldova','Monaco','Mongolia','Montserrat','Morocco','Mozambique',
        'Namibia','Nauru','Nepal','Netherlands','Netherlands Antilles','New Caledonia','New Zealand',
        'Nicaragua','Niger','Nigeria','N. Mariana Islands','Norway','Oman','Pakistan','Palau','Panama',
        'Papua New Guinea','Paraguay','Peru','Philippines','Poland','Portugal','Puerto Rico','Qatar',
        'Reunion','Romania','Russia','Rwanda','Saint Helena','Saint Kitts & Nevis','Saint Lucia',
        'St Pierre & Miquelon','Saint Vincent and the Grenadines','Samoa','San Marino',
        'Sao Tome & Principe','Saudi Arabia','Senegal','Serbia','Seychelles','Sierra Leone',
        'Singapore','Slovakia','Slovenia','Solomon Islands','Somalia','South Africa','Spain',
        'Sri Lanka','Sudan','Suriname','Swaziland','Sweden','Switzerland','Syria','Taiwan',
        'Tajikistan','Tanzania','Thailand','Togo','Tonga','Trinidad & Tobago','Tunisia','Turkey',
        'Turkmenistan','Turks & Caicos Is','Tuvalu','Uganda','Ukraine','United Arab Emirates',
        'United Kingdom','United States','Uruguay','Uzbekistan','Vanuatu','Venezuela','Vietnam',
        'Virgin Islands','Wallis and Futuna','West Bank','Western Sahara','Yemen','Zambia','Zimbabwe'
    ]

    matching = countries.filter((country) => {
        return country.toLowerCase().startsWith(value.toLowerCase())
    })

    callback(matching)
}
Enter fullscreen mode Exit fullscreen mode

The getSuggestions function simulates AJAX requests and find any countries begin with typed characters. Then, it will invoke the callback function, passing the matching country list.

Implementation of the callback function onMatchingData will differentiate the two methods we discussed above. Let's implement them separately.

Method 1:

Add a datalist element below the input box with id attribute countryList. Set the same value for the list attribute of the input box.

<div style="margin-top: 5px;">
    <!-- list attribute value is same as of datalist id attribute -->
    <input list="countryList" type="text" name="country" id="country">
    <datalist id="countryList"></datalist>
</div>
Enter fullscreen mode Exit fullscreen mode

Create a global variable (dataListElement) that holds the datalist element and assign its value when document is loaded.

// ...
let dataListElement = null
// ... more code
document.addEventListener('DOMContentLoaded', (e) => {
    inputBox = document.getElementById('country')
    dataListElement = document.getElementById('countryList')
// ... more code
Enter fullscreen mode Exit fullscreen mode

The clearSuggestions function will clear the datalist element's option list, if any.

const clearSuggestions = () => {
    dataListElement.innerHTML = ''
}
Enter fullscreen mode Exit fullscreen mode

Now we can populate the option list with matching countries.

const onMatchingData = (suggestions) => {
    let options = ''

    suggestions.forEach((country) => {
        options += '<option value="' + country + '">'
    })

    dataListElement.innerHTML = options
}
Enter fullscreen mode Exit fullscreen mode

Final code layout and output as below.
Code Layout - Method 1

Final Output - Method 1

Method 2:

First, add a new div element below the input box with id attribute countries and class attribute with hide (class hide will hide this div at document load).

<div style="margin-top: 5px;">
    <input type="text" name="country" id="country">
    <div id="countries" class="hide"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Store the newly added div element in a global variable (countryListDiv) when document is loaded.

// ...
let countryListDiv = null
// ... more code
document.addEventListener('DOMContentLoaded', (e) => {
    inputBox = document.getElementById('country')
    countryListDiv = document.getElementById('countries')
// ... more code
Enter fullscreen mode Exit fullscreen mode

Similar to Method 1, clearSuggestions function will clear the countryListDiv inner divs and hide it.

const clearSuggestions = () => {
    countryListDiv.innerHTML = ''
    countryListDiv.classList.add('hide')
}
Enter fullscreen mode Exit fullscreen mode

We need a helper function (setCountry) to set the input box value whenever a user clicks on an entry of country list div. After setting the input box value, we'll clear the suggestions.

const setCountry = (country) => {
    inputBox.value = country
    clearSuggestions()
}
Enter fullscreen mode Exit fullscreen mode

The callback function onMatchingData slightly different here. We create a new entry for each matching country and add onclick event to call our helper function with the country name. The suggestion list will be shown only if there are matching countries (suggestions.length > 0).

const onMatchingData = (suggestions) => {
    let countries = ''

    suggestions.forEach((country) => {
        countries += ('<div class="item" onclick="setCountry(\'' +
                        country + '\')"> ' + country + '</div>')
    })

    countryListDiv.innerHTML = countries
    if ( suggestions.length > 0 ) {
        countryListDiv.classList.remove('hide')
    }
}
Enter fullscreen mode Exit fullscreen mode

Final code layout and output shown below.

Code Layout - Method 1
Final Output - Method 2

Top comments (0)