DEV Community

michael-neis
michael-neis

Posted on • Updated on

The JavaScript Iceberg

A dropdown menu. Seems like a pretty easy web component to create right? Yes, yes it is.
A text input with autofill capabilities? Piece of cake with CSS.
Putting those two things together into one efficient and alluring DOM element? Not on your life.

If you are just getting into coding, like me, you may have experienced what many refer to as the iceberg effect. What may seem like a small, insignificant little piece of user interface or page functionality can end up making up half of your code. Or at least it will if you don't use all of the resources at your disposal.

While creating a web application for a project phase at Flatiron School, I set out to create what I initially thought would be a simple HTML element with some fancy CSS styling. I wanted to create a text input box with a dropdown of searchable words in my application, but only have those words appear if they matched the letters that were being typed. You've probably seen this kind of thing before.

One very important thing to keep in mind is that at the time of making this, all I knew was basic JavaScript, HTML and some CSS styling. Now, I had found out that there were some options that CSS gave me in terms of how to style a form. They were limited, but I thought I could make do. The ability to create an autofill text box? Check. But those options would only include words you have already typed. The ability to create a dropdown of viable options? Yes again. Unfortunately, there was no way to combine these two CSS elements into what I had dreamed of. So, I turned to JavaScript to solve my problems. And while I did eventually find an answer (with a lot of help from Google, W3Schools and Stack Overflow), the code was not nearly as concise as I had initially thought. I'll just let you see for yourself:

function autoFillBoxes (text, array){
    let selectedWord;
    text.addEventListener('input', function(e) {
let dropDown
let listItem
let matchLetters = this.value;

closeAllLists();

if (!matchLetters) {return false;}

selectedWord = -1;

dropDown = document.createElement('div');
dropDown.setAttribute('id', this.id + "selectorsList");
dropDown.setAttribute('class', 'selectorsItems');

this.parentNode.appendChild(dropDown);

for (let i = 0; i < array.length; i++){
if (array[i].substr(0, matchLetters.length).toUpperCase() == 
matchLetters.toUpperCase()){ 
listItem = document.createElement('div'); 
listItem.innerHTML = "<strong>" + array[i].substr(0, 
matchLetters.length) + "</strong>";
listItem.innerHTML += array[i].substr(matchLetters.length);


listItem.innerHTML += "<input type='hidden' value='" + array[i] + 
"'>";

listItem.addEventListener('click', function(e) {

text.value = this.getElementsByTagName('input')[0].value;

selectedWord = -1;

closeAllLists();
})
listItem.setAttribute('class', 'autoListOptions')
dropDown.appendChild(listItem);
            }
        }
    })
text.addEventListener('keydown', function(keySpec) {
let wordsArray= document.getElementById(this.id + "selectorsList");

if (wordsArray) wordsArray= 
wordsArray.getElementsByTagName('div');

if (keySpec.keyCode == 40){
selectedWord++;
addActive(wordsArray);
} else if (keySpec.keyCode == 38){
selectedWord--;
addActive(wordsArray);
} else if (keySpec.keyCode == 13){
   if (selectedWord > -1){
   keySpec.preventDefault();
   if (wordsArray) wordsArray[selectedWord].click();
   selectedWord = -1;
    }
  }
});

function addActive(wordsArray){
if (!wordsArray) return false;
removeActive(wordsArray);

if (selectedWord >= wordsArray.length) selectedWord = 0;
if (selectedWord < 0) selectedWord = (wordsArray.length - 1);

wordsArray[selectedWord].classList.add('activeSelectors');
}

function removeActive(wordsArray){
for (let i = 0; i < wordsArray.length; i++){
    wordsArray[i].classList.remove('activeSelectors');
   }
}
function closeAllLists() {
var dropDown = document.getElementsByClassName("selectorsItems");
for (var i = 0; i < dropDown.length; i++) {
   dropDown[i].parentNode.removeChild(dropDown[i]);
    }
}

document.addEventListener('click', (e) => closeAllLists(e.target))
}
Enter fullscreen mode Exit fullscreen mode

Wowza. Not exactly a quaint little web component now is it? Let me break this down a bit and explain how everything works.

First off, we have to determine what it is we are passing into this beast. Our text variable is the text we are typing into the form. We can target this specifically by assigning an id to the form element in HTML:

      <div class="autoComplete">
<input type="text" id="textInput" class="selectors" name="input"/>
      </div>
Enter fullscreen mode Exit fullscreen mode

(The div and input classes will come in handy later, for now we're just focused on the input id)

And assigning the value of that HTML element to a variable in JS:

const textToPass = document.getElementById('textInput')
Enter fullscreen mode Exit fullscreen mode

Cool, now we will be able to call an 'input' event listener on textToPass, as well as extract the value from it. The second variable we are passing represents an array. This array is filled with strings of all of the possible words you want to have populate the dropdown. It can be filled with anything of your choosing, just as long as they are strings:

const arrayToPass = ['These', 'are', 'the', 'words', 'you', 
'can', 'choose', 'from']
Enter fullscreen mode Exit fullscreen mode

Now lets go back and take a look at the first chunk of that whole function:

function autoFillBoxes (text, array){
    let selectedWord;
    text.addEventListener('input', function(e) {
Enter fullscreen mode Exit fullscreen mode

Note: this selectedWord variable will come in handy later, it will be the variable that determines which word in our dropdown is being focused on.

As you can see, we are passing in a text and array variable. When we initiate this function, we will use our textToPass and arrayToPass variables in these fields.

We then see our first big event listener to kick off the bulk of our function. input is a listener on text that will initiate the function(e) whenever a user adds an input (aka types) in its field. Now let's take a look at the function being initiated:

let dropDown
let listItem
let matchLetters = this.value;

closeAllLists();

if (!matchLetters) {return false;}

selectedWord = -1;

dropDown = document.createElement('div');
dropDown.setAttribute('id', this.id + "selectorsList");
dropDown.setAttribute('class', 'selectorsItems');

this.parentNode.appendChild(dropDown);

for (let i = 0; i < array.length; i++){
if (array[i].substr(0, matchLetters.length).toUpperCase() == 
matchLetters.toUpperCase()){ 
listItem = document.createElement('div'); 
listItem.innerHTML = "<strong>" + array[i].substr(0, 
matchLetters.length) + "</strong>";
listItem.innerHTML += array[i].substr(matchLetters.length);


listItem.innerHTML += "<input type='hidden' value='" + array[i] + 
"'>";

listItem.addEventListener('click', function(e) {

text.value = this.getElementsByTagName('input')[0].value;

selectedWord = -1;

closeAllLists();
})
listItem.setAttribute('class', 'autoListOptions')
dropDown.appendChild(listItem);
            }
        }
    })
Enter fullscreen mode Exit fullscreen mode

There's a lot that is happening here. First, we are declaring three variables. matchLetters is assigned the value of this.value. The this keyword refers to the object it is in, in our case being text. (text.value would give use the same result, but using this allows for more dynamic and reusable code). dropDown and listItem are two variables that as you can see further down become divs using the .createElement() method. The closeAllLists() function, which we will define in detail later, makes sure that aby previous lists are closed before appending our new divs to the text parent node.

The dropDown div is the container for all of the words we want to populate in our dropdown options, and the listItem divs are the divs containing each specific word. Towards the bottom, we append each listItem div that we have created to our dropDown div.

In order to use CSS styling and refer to each div later in our function, each div must have ids and/or class names. dropDown is given a class name of "selectorsItems" and an id of this.id + "selectorsList" (there's that this keyword again, grabbing the id from our text). The listItems are all given a class name of "autoListOptions", but no id, since they will all behave the same way.

In our for loop, we are checking to see if every word in our array matches our if statement. In that if statement, we are using .substr for a given word in our array from 0 to matchLetters.length. Remember, matchLetters is the text the user has typed, so we are making sure only to check on the same amount of letters as letters we have typed. We are then comparing those letters to the letters of matchLetters itself using ===. We have to add .toUpperCase() to ensure that neither the word from the array nor the letters being typed are case sensitive. Since we are using a for loop, any of the words in our array that satisfy that if statement will be passed into the function. We don't need an else statement, because if no words match our letters, we don't need anything to happen.

Now, we could just add that matching array string to a listItem and call it a day, but it would be so much cooler if we added a little more flare than that. Again, we can fill the inner HTML of listItem first with the letters that we have typed using .substr(0, matchLetters.length) (we know these will match, otherwise thee if statement would have failed). Adding a <strong> tag will make these letters bold. We then fill the rest of the inner HTML using += and starting our .substr at our current amount of letters. With no end point defined, this will just fill until the end of the string.

Next, we have to give that newly created div a hidden input and a value. The hidden input will allow us to call an event listener on the div to access its value. We can then add a click event listener on our listItem and employ an anonymous function. That function will set the text.value (the text in our original input field) to equal the value found by searching for that hidden input within this (our listItem) div. selectedWord = -1 and closeAllLists() here are used to clear and reset our function.

Now, what we could do here is just define our closeAllLists function and call it a day. At this point, we are able to create a dropdown of autofill words from our array and click on them to fill our text box. But we can go a step further, by allowing the user to scroll through and select words using the arrow keys. This is where our selectedWord variable will finally come in handy.

text.addEventListener('keydown', function(keySpec) {
let wordsArray= document.getElementById(this.id + "selectorsList");

if (wordsArray) wordsArray= 
wordsArray.getElementsByTagName('div');

if (keySpec.keyCode == 40){
selectedWord++;
addActive(wordsArray);
} else if (keySpec.keyCode == 38){
selectedWord--;
addActive(wordsArray);
} else if (keySpec.keyCode == 13){
   if (selectedWord > -1){
   keySpec.preventDefault();
   if (wordsArray) wordsArray[selectedWord].click();
   selectedWord = -1;
    }
  }
});

function addActive(wordsArray){
if (!wordsArray) return false;
removeActive(wordsArray);

if (selectedWord >= wordsArray.length) selectedWord = 0;
if (selectedWord < 0) selectedWord = (wordsArray.length - 1);

wordsArray[selectedWord].classList.add('activeSelectors');
}

function removeActive(wordsArray){
for (let i = 0; i < wordsArray.length; i++){
    wordsArray[i].classList.remove('activeSelectors');
   }
}
Enter fullscreen mode Exit fullscreen mode

Here, we are giving our text box a 'keydown' event listener, and passing a function focusing on the event cause, in our case we call that keySpec. We then want to create an array of HTML elements to sort through. To do so, we first want to declare our wordsArray to equal the dropDown div, then we need to go a step further and set the value of wordsArray to be every div element within the dropDown div. Now we have our collection of listItem HTML divs stored as an array.

The if, else if, else if statement that follows ensures that we are only passing this function if specific buttons are being pressed. We check our keySpec.keyCode to do so. Every keyboard button has a code, and .keyCode will return us that code (as a number). The keycode for the down arrow key is 40, the keycode for the up arrow is 38, and the keycode for the enter key is 13. If the down arrow key is pressed, selectWord is incremented, and if the up arrow is pressed, selectWord is decremented. In either case, the array is passed into our addActive function. This function will add a class attribute of activeSelectors to our divs so that they can be independently styled, as well as use the value of our selectedWord to sort through our array.

As you can see at the end of our addActive function, we will be applying that activeSelectors class element to whatever div is at the index of our array with the same value as selectedWord using wordsArray[selectedWord]. Because selectedWord starts at -1 for every input of text, an initial down arrow keydown will increment it to 0, making this bit of code wordsArray[0]. Another down arrow will make it wordsArray[1] and so on. The same is true of an up arrow keydown, which would change something like wordsArray[3] to wordsArray[2]. But as you may have already wondered, what happens if the up arrow is pressed first? Or what happens if selectedWord becomes a number that is longer than our array? And how do we remove the active designation once we are done with it? Well, that is what the beginning of our addActive function is for.

The first two things we want to do in our addActive function is ensure that the array we are passing has a truthy value (not undefined or null) and pass a removeActive function. This removeActive function will go through our entire wordsArray and remove any 'activeSelectors' so that we stay focused on one div. Next we have to make sure our selectedWord value never becomes a number that is not useful to us. If the user 'down arrow's all the way to the bottom of the dropdown div, and then keeps hitting 'down arrow,' we want to change the selectedWord value back to 0 so that they can start from the beginning again. The same is true for 'up arrow', but this time since selectedWord would become less than 0, we want to change it to equal the last element of the array (aka wordsArray.length -1).

Now we can finally declare that closeAllLists function that we have been using.

function closeAllLists() {
var dropDown = document.getElementsByClassName("selectorsItems");
for (var i = 0; i < dropDown.length; i++) {
   dropDown[i].parentNode.removeChild(dropDown[i]);
    }
}

document.addEventListener('click', (e) => closeAllLists(e.target))
Enter fullscreen mode Exit fullscreen mode

We have to redeclare our dropDown variable since we are now in a different scope of the function. It will point to the same div, with a class name of 'selectorsItems'. We are then stating that for every element in dropDown, remove that child element from dropDown. Then we add a click event listener to the entire document so that when a user clicks anywhere, the div is cleared (including when the user clicks on the word itself).

The only thing left now is to initiate it:

autoFillBoxes(textInputField, arrayToCheck)
Enter fullscreen mode Exit fullscreen mode

Those values should obviously be your own, based on the text input field and array you want to use.

The HTML formatting and CSS styling are now largely up to you, but there are a few things that need to be in place in order for all of this to work:

In HTML:
The form that your target input element is in must have autocomplete set to "off."

<form id="exampleSelection" autocomplete="off">
Enter fullscreen mode Exit fullscreen mode

You must also make sure that you add an easy to remember id and class to your input to target.

In CSS:
Your base HTML input element should have position: relative, and the div you create in your function should have position: absolute (It's easiest to set these using their class name).

In your activeSelectors styling (the divs that are considered 'active' as the user uses up arrow/down arrow), make sure that the background color is marked as !important.

.activeSelectors{
    background-color: red !important;
}
Enter fullscreen mode Exit fullscreen mode

Any other styling is up to you.

Conclusion

Coding can be a lot of fun, but it can also be incredibly frustrating and time consuming. Some things that make complete sense in our mind may not translate that easily into your computer. Managing and tempering expectations can be an important skill to master when starting projects, because sometimes the reality of making a goal happen may not always be worth its time.

Top comments (0)