🛠️ Tabby: Refactoring

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ 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:

  1. tab
  2. tabs
  3. tabContent
  4. tabContents

We can get them from these locations:

  1. tab: from event.target
  2. tabs: from the global scope
  3. tabContent: from tab
  4. 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:

  1. Decide whether we should get previous or next tab
  2. Find the previous or next tab
  3. 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

  1. tabs
  2. 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:

  1. If key is left, we grab the previous tab
  2. 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.