Smart input box with auto-suggestions for the users (two methods)
Tags: html, javascript, input, webdev
Published: 2022-06-26 10:09:22

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.

```html
<!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>
```

Here is the common CSS file for both implementations:

```css
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;
}
```

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

```javascript
let inputBoxOldVal = '' // holds the previous value of input box
let inputBox = null
```

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.

```javascript
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()
		}
	})
})
```

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.

```javascript
// 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)
}
```

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.

```html
<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>
```

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


```javascript
// ...
let dataListElement = null
// ... more code
document.addEventListener('DOMContentLoaded', (e) => {
	inputBox = document.getElementById('country')
	dataListElement = document.getElementById('countryList')
// ... more code
```

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

```javascript
const clearSuggestions = () => {
	dataListElement.innerHTML = ''
}
```

Now we can populate the `option` list with matching countries.

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

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

	dataListElement.innerHTML = options
}
```

Final code layout and output as below.
![Code Layout - Method 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpbufiot3e2g25itx8u8.png)

![Final Output - Method 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3idhymbhxo6oslfvzb2.png)

### 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).

```html
<div style="margin-top: 5px;">
	<input type="text" name="country" id="country">
	<div id="countries" class="hide"></div>
</div>
```

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

```javascript
// ...
let countryListDiv = null
// ... more code
document.addEventListener('DOMContentLoaded', (e) => {
	inputBox = document.getElementById('country')
	countryListDiv = document.getElementById('countries')
// ... more code
```

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

```javascript
const clearSuggestions = () => {
	countryListDiv.innerHTML = ''
	countryListDiv.classList.add('hide')
}
```

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.

```javascript
const setCountry = (country) => {
	inputBox.value = country
	clearSuggestions()
}
```

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`).

```javascript
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')
	}
}
```

Final code layout and output shown below.

![Code Layout - Method 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0slprkhvrehj66yigyc2.png) 
![Final Output - Method 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m1rqd66fksa2lk21jxq4.png)