The JavaScript Iceberg
Tags: javascript, react, html, css
Published: 2021-11-05 20:56:56

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:

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

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:
```javascript
      <div class="autoComplete">
<input type="text" id="textInput" class="selectors" name="input"/>
      </div>
```
(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:
```javascript
const textToPass = document.getElementById('textInput')
```

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:
```javascript
const arrayToPass = ['These', 'are', 'the', 'words', 'you', 
'can', 'choose', 'from']
```

Now lets go back and take a look at the first chunk of that whole function:
```javascript
function autoFillBoxes (text, array){
    let selectedWord;
    text.addEventListener('input', function(e) {
```
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:

```javascript
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);
            }
        }
    })
```
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 <strong>bold</strong>. 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. 

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

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

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:
```javascript
autoFillBoxes(textInputField, arrayToCheck)
```
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:

<strong>In HTML:</strong>
The form that your target input element is in must have autocomplete set to "off."
```javascript
<form id="exampleSelection" autocomplete="off">
```
You must also make sure that you add an easy to remember id and class to your `input` to target.

<strong>In CSS:</strong>
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.
```javascript
.activeSelectors{
    background-color: red !important;
}
```
Any other styling is up to you.

<h2>Conclusion</h2>
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.