I searched for a standard solution for creating autocomplete input field. To my surprise, there were good no pure javascript solutions. I tried an autocomplete libary, but couldn't get it to work for ajax calls. It required data to be prefetched. So I ended up writing my solution, which worked on live ajax calls.
In the below approach, we will handle the following problems.
keyup
We can add this section anywhere on the page. The below section can be added inside an HTML form
element. The autocompleted value will be sent in the form submit, as it is an input
field. The form can be submitted using ajax or through a regular POST server call.
<section class="control" id="searchbox-container">
<input class="input is-dark" type="text" name="city" placeholder="Where were you born?" id="searchbox" autocomplete="off" />
<div class="control" id="dropdown-menu"></div>
</section>
When calling ajax on keyup
, use a timeout function to not immediately send the ajax call. Another important check is DO NOTHING when input
box is empty.
const searchBox = document.getElementById('searchbox');
const dropdownMenu = document.getElementById('dropdown-menu');
const searchBoxContainer = document.getElementById('searchbox-container');
searchBox.addEventListener("keyup", function(event) {
const timer = setTimeout(function () {
var sr = event.target.value;
if(!sr) return; //Do nothing for empty value
searchBoxContainer.classList.add("control", "is-loading");
const request = new Request('https://api.metamug.com/covid/v1.1/city?q='+sr+'&limit=7');
fetch(request)
.then((response) => response.json())
.then((data) => {
if (searchBox.value) { //src not cleaned, backspace removed
dropdownMenu.replaceChildren(searchResult(data));
}
searchBoxContainer.classList.remove("is-loading");
});
}, 500);
});
In each ajax response, we will remove all the fields and add the ajax response. Instead of using dropdownMenu.innerHTML=''
along with appendChild
, we have used replaceChildren
as it is native API available
Below API is available, which has been used for this example.
https://api.metamug.com/covid/v1.1/city?q=mumbai&limit=7
The dropdown is an unordered list ul
with each record as li
. In this code we have avoided using data-
attributes and added events on each of the li
elements.
function searchResult(result){
const ul = document.createElement('ul')
ul.classList.add('box', 'mt-1' );
const loc = result.loc1.concat(result.loc2);
loc.forEach((x)=>{
if(!x)return;
ul.appendChild(createListItem(x))
})
return ul;
}
Since we are adding an event on each of the elements in the dropdown, we must disable event propogation to avoid event bubbling to its parents.
function createListItem(x){
const li = document.createElement('li')
li.classList.add('py-1'); //, 'pl-5', 'is-capitalized')
//li.dataset.lat = x.lat;
//li.dataset.lon = x.lon;
//li.dataset.tz = x.tz;
li.innerText = x.name
const selectEvent = function(event){
event.stopPropagation(); //parent elements will not have the same event
const li = event.target
clearDropdown();
searchBox.value = x.name;
};
li.addEventListener('click', selectEvent)
li.addEventListener('touchstart', selectEvent)
return li
}
Along with the click event, a touch event is also added for mobile devices.
Do clear the dropdown, when the input box is empty. setInterval
here is used to keep checking for the input box for blank.
function clearDropdown(){
//dropdownMenu.replaceChildren(); https://stackoverflow.com/a/65413839/1097600
dropdownMenu.innerHTML = '';
searchBoxContainer.classList.remove("is-loading");
}
//keep checking for an empty search box every 5 seconds
setInterval(function() {
if (!searchBox.value) { //empty search box
clearDropdown()
}
}, 500);
You can find the complete Autocomplete JS code here.
.control {
box-sizing: border-box;
clear: both;
font-size: 1rem;
position: relative;
text-align: inherit;
}
.mt-1 {
margin-top: 0.25rem!important;
}
.box {
background-color: #fff;
border-radius: 6px;
box-shadow: 0 0.5em 1em -0.125em rgb(10 10 10 / 10%), 0 0 0 1px rgb(10 10 10 / 2%);
color: #4a4a4a;
display: block;
padding: 1.25rem;
}
ul {
list-style: none;
}
.input, .textarea {
box-shadow: inset 0 0.0625em 0.125em rgb(10 10 10 / 5%);
max-width: 100%;
width: 100%;
}
.control.is-loading::after {
position:absolute!important;
right:.625em;
top:.625em;
z-index:4
}