🛠️ Tabby: Refactoring
We wrote two chunks of code for Tabby. One for listening to a user’s click. Another for listening to Left and Right arrow keys.
We can simplify these two events.
The click event
Here’s the code for the click event:
tabsList.addEventListener('click', event => {
const tab = event.target
const target = tab.dataset.target
const tabContent = tabby.querySelector('#' + target)
// Selects a tab
tabs.forEach(t => {
t.classList.remove('is-selected')
t.setAttribute('tabindex', '-1')
})
tab.classList.add('is-selected')
tab.removeAttribute('tabindex')
// Selects the corresponding tab content
tabContents.forEach(c => c.classList.remove('is-selected'))
tabContent.classList.add('is-selected')
})
Most of the code here is used to select a tab (and its tab-contents). We can group these code into a function called selectTab
.
const selectTab = _ => {
// Do something
}
First, let’s copy-paste everything over.
const selectTab = _ => {
const tab = event.target
const target = tab.dataset.target
const tabContent = tabby.querySelector('#' + target)
// Selects a tab
tabs.forEach(t => {
t.classList.remove('is-selected')
t.setAttribute('tabindex', '-1')
})
tab.classList.add('is-selected')
tab.removeAttribute('tabindex')
// Selects the corresponding tab content
tabContents.forEach(c => c.classList.remove('is-selected'))
tabContent.classList.add('is-selected')
}
If you look at the code, you know we need these variables:
tab
tabs
tabContent
tabContents
We can get them from these locations:
tab
: from event.target
tabs
: from the global scope
tabContent
: from tab
tabContents
: from the global scope
This means: We only need the tab
variable in selectTab
. We can pass tab
directly into selectTab
. We don’t have to introduce the entire event object.
const selectTab = tab => {
const target = tab.dataset.target
const tabContent = tabby.querySelector('#' + target)
// ...
}
Using selectTab
:
tabsList.addEventListener('click', event => {
const tab = event.target
selectTab(tab)
})
It became much easier to understand what the click
event handler does!
The keydown event
Here’s what we wrote for switching tabs with Left and Right arrow keys.
document.addEventListener('keydown', event => {
const { key } = event
if (key !== 'ArrowLeft' && key !== 'ArrowRight') return
if (!event.target.matches('.tab')) return
const index = tabs.findIndex(t => t.classList.contains('is-selected'))
let targetTab
if (key === 'ArrowLeft' && index !== 0) targetTab = tabs[index - 1]
if (key === 'ArrowRight' && index !== tabs.length - 1)
targetTab = tabs[index + 1]
if (targetTab) targetTab.click()
})
In the first three lines, we checked whether we want to act on the event. These three lines cannot be moved anywhere else.
document.addEventListener('keydown', event => {
const { key } = event
if (key !== 'ArrowLeft' && key !== 'ArrowRight') return
if (!event.target.matches('.tab')) return
// ...
})
The next few lines are used to get the target tab.
document.addEventListener('keydown', event => {
// ...
const index = tabs.findIndex(t => t.classList.contains('is-selected'))
let targetTab
if (key === 'ArrowLeft' && index !== 0) targetTab = tabs[index - 1]
if (key === 'ArrowRight' && index !== tabs.length - 1)
targetTab = tabs[index + 1]
})
Here, we tried to:
- Decide whether we should get previous or next tab
- Find the previous or next tab
- Trigger a click event
To simplify the code, we can create getPreviousTab
and getNextTab
functions.
const getPreviousTab = _ => {
// Do something
}
const getNextTab = _ => {
// Do something
}
First, let’s copy-paste the code we used to get the previous and next tabs.
const getPreviousTab = _ => {
if (key === 'ArrowLeft' && index !== 0) targetTab = tabs[index - 1]
}
const getNextTab = _ => {
if (key === 'ArrowRight' && index !== tabs.length - 1)
targetTab = tabs[index + 1]
}
The purpose of getPreviousTab
is to find the previous tab. Likewise for getNextTab
. The key a user pressed should not matter, so let’s remove the key
part.
const getPreviousTab = _ => {
if (index !== 0) targetTab = tabs[index - 1]
}
const getNextTab = _ => {
if (index !== tabs.length - 1) targetTab = tabs[index + 1]
}
This makes more sense.
We need to return the previous tab in getPreviousTab
. We also need to return the next tab in getNextTab
.
const getPreviousTab = _ => {
if (index !== 0) {
return tabs[index - 1]
}
}
const getNextTab = _ => {
if (index !== tabs.length - 1) {
return tabs[index + 1]
}
}
You can tell we need two variables for both getPreviousTab
and getNextTab
tabs
index
We can get tabs
from the global scope, so we only need to pass in index
to getPreviousTab
and getNextTab
.
const getPreviousTab = index => {
if (index !== 0) {
return tabs[index - 1]
}
}
const getNextTab = index => {
if (index !== tabs.length - 1) {
return tabs[index + 1]
}
}
Using getPreviousTab
and getNextTab
:
document.addEventListener('keydown', event => {
// ...
const index = tabs.findIndex(t => t.classList.contains('is-selected'))
let targetTab
if (key === 'ArrowLeft') targetTab = getPreviousTab(index)
if (key === 'ArrowRight') targetTab = getNextTab(index)
// ...
})
This makes more sense now. It reads:
- If key is left, we grab the previous tab
- If key is right, we grab the next tab
There’s one final improvement we can make. When we found targetTab
, we triggered a “click” with it.
document.addEventListener('keydown', event => {
// ...
if (targetTab) {
targetTab.click()
}
})
If you read this code, you understand we triggered a click
event, which selects the appropriate tab with selectTab
.