The JavaScript Iceberg
michael-neis
Posted on November 5, 2021
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))
}
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>
(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')
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']
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) {
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);
}
}
})
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');
}
}
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))
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)
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">
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;
}
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.
Posted on November 5, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
March 26, 2023