Earlier this week, I got an email telling me that the Odin Project had just upgraded their JavaScript course. I checked it out. If you're like me and have been putting off learning more about JavaScript because of all of the build tooling, this course is definitely for you. I learned the basics of webpack (and even submitted a pull request to make a correction on the webpack tutorial!), and it was all at a super gentle and beginner-friendly pace. As I was going through this course, during one of the assignments, I came across a pattern that I thought was neat. Especially after my post a couple of weeks ago about closures, I knew I had to share it. If at any point you're reading through my code examples and you get furious at my code, please skip ahead to the Delirium Disclaimer.
The Project
The goal was to use as many different modules as possible in order to test out the bundling power of webpack. I was supposed to create a generic landing page for a restaurant, and it had to have a tab-based navigation system. Here's the site I came up with. (and the related GitHub repo)
I didn't do anything to make it look pretty on mobile, so if you're reading on mobile, forgive me.
The Technique
The technique I want to share is the one I used for the nav button click callback: I created a closure! Let me back up. I've got three buttons. The HTML ends up looking something like this:
<div class="tabs">
<button class="tabs__link active" data-target="About">About</button>
<button class="tabs__link" data-target="Menu">Menu</button>
<button class="tabs__link" data-target="Contact">Contact</button>
</div>
I then have a bunch of <div class="tabcontent">
's that contain the content of the tabs. Every one but the active one has display: hidden
, so only the active one will show up.
Of course, the assignment specifically asked me to generate these buttons in JavaScript, so it ends up looking more like this:
// Don't worry about openTab now.
// We'll talk about it in a minute.
import openTab from './openTab';
const loadNav = () => {
const tabHolder = document.querySelector('.tabs');
const tabs = ['About', 'Menu', 'Contact'];
tabs.forEach(tabName => {
const button = document.createElement('button');
button.classList.add('tabs__link');
button.dataset.target = tabName;
button.addEventListener('click', openTab(tabName));
button.innerHTML = tabName;
tabHolder.appendChild(button);
});
};
But here's where the magic happens. I'll show you the code for openTab
, and then I'll talk about what's so special about it.
const openTab = tabName => {
return (e) => {
const tabContent = document.querySelectorAll('.tabcontent');
tabContent.forEach(tab => {
tab.style.display = "none";
});
const tabLinks = document.querySelectorAll('.tabs__link');
tabLinks.forEach(link => {
link.classList.remove('active');
});
const activeTab = document.querySelector(`[data-page="${tabName}"]`);
activeTab.style.display = "block";
e.currentTarget.classList.add('active');
};
};
export default openTab;
So What's Going On Here?
Usually, when you pass a callback function to an event listener, you do it without parenthesis, like this: button.addEventListener('click', doTheThing)
. This is because you're not calling the function as you're creating the event listener, you're passing the function object to be called later. However, have you ever wanted to pass additional information to a callback? Usually when you have a callback function for event listeners, they only take the event as an argument:
const doTheThing = e => {
// stuff
};
However, what if you want it to have additional information?
const doTheThing = (e, myColor) => {
console.log(myColor);
};
In my case, I wanted to write one callback function that would work for all three nav buttons, even though their functionality would each be a little different, based on which tab they were trying to act on. So I needed something like this:
const openTab = (e, tabName) => {
// The stuff
};
BUT, if you try this, JavaScript gets grumpy. So what can we do? One solution is to create a closure at the time that you add the event listener.
const openTab = tabName => {
return e => {
// Things in here have access to tabName *and* e
}
}
When you use it like this:
button.addEventListener('click', openTab(tabName));
the openTab
function gets immediately evaluated, and the new, anonymous function is given as the callback. It's the same as writing:
button.addEventListener('click', e => {
console.log(tabName + "Haha!");
});
Thanks to our friend the closure, the anonymous function placed after the event listener retains access to the tabName
variable, even though the function was called long before the event ever fires. If you're not exactly sure what a closure is, definitely take a look at my post on closures. The benefit is that you can pull the openTab
logic out into its own function and your addEventListener
call ends up looking a lot cleaner.
So, the next time you want your callback functions to have more information than just the event passed in, consider using a closure to DRY things up.
Delirium Disclaimer
As I was writing this post, I noticed a lot of things I should change and fix in my original code (variable name consistencies, CSS class name consistencies, etc.). I also noticed that I probably could have left out the tabName
variable completely and gotten away with getting everything that I needed from the event
that got passed into the function. The whole closure thing may have been unnecessary.
I'm going to go ahead and blame this on the fact that by the time I got to this part of the code, I was delirious from all of the things I was doing and new things I was learning. Now that I've had some sleep, past-me's code is making me cringe a little bit. Sorry!
That being said, this is one of my first real stabs at modern JavaScript. So if you see ways that I could improve my code or do something more idiomatically, I'd love to get your feedback. Definitely share your wisdom!
Originally posted on assert_not magic?
Top comments (4)
Hey! I'm the guy that wrote most of that JavaScript course... and this feedback is REALLY encouraging! I'm glad you enjoyed it, and I'm really happy to see what you were able to make as a result of that lesson.
Of course there are simpler ways of creating tab layouts, and I'm sure someone will jump into the comments to explain how you've over-complicated things.. but as you've found out the point of the lesson is exactly what you've learned along the way. Congratulations! π
Thatβs awesome! Thanks for the guide and the feedback. Yep, mostly, I was just happy to learn how modules work, and how they work with webpack. I think Iβve got some ideas on how to make my life easier next time I make tabs. ππ»
Oh man, I finally found a convincing example to curry functions in javascript. I'm not sure why you call it closure though (I've never been able to wrap my head around the word - so many different explanations). But this is definitely function currying.
Thanks for this amazing example :)
Hey, glad you liked it, thanks for the feedback!
As far as I know, a closure is anytime you wrap a function in an outer scope and return it, such that that function has access to the outer scope long after that outer scope has ended.
After doing some reading, it looks like currying is when you break a multi-parameter function into a series of nested functions and you end up only passing in one thing at a time? In which case, it would seem like currying is just one specific example in the broader case of closures. Let me know if that sounds right or if I'm missing something.
If you haven't already, you might like to take a look at an article I wrote a few weeks ago that goes more into detail about closures. The code examples are in Python, but I think the point still comes across.
Thanks again!