home

OOP / Functional Tabber Implementations

'Tabber' is a ui widget where a set of buttons toggle content visible in a display area, see examples below

Here's the html for the tabber

<div class="Tabs">
  <div class="Tabs-buttons">
    <button class="Tabs-button js-Tabber-button is-active" data-tab-index="0">Tab 1</button>
    <button class="Tabs-button js-Tabber-button" data-tab-index="1">Tab 2</button>
    <button class="Tabs-button js-Tabber-button" data-tab-index="2">Tab 3</button>
  </div>
  <div class="Tabs-bodies">
    <div class="Tabs-body js-Tabber-body is-active" data-tab-index="0">Tab Content 1</div>
    <div class="Tabs-body js-Tabber-body" data-tab-index="1">Tab Content 2</div>
    <div class="Tabs-body js-Tabber-body" data-tab-index="2">Tab Content 3</div>
  </div>
</div>

OOP Implementation

Tab Content 1
Tab Content 2
Tab Content 3
class Tabber {
  constructor (el) {
    this.el = el;
    this.tabs = [];

    const buttons = this.el.querySelectorAll('.js-Tabber-button');
    const contents = this.el.querySelectorAll('.js-Tabber-body');
    for (const button of this.buttons) {
      for (const content of this.contents) {
        if (button.dataset.tabIndex === content.dataset.tabIndex) {
          this.tabs.push({button, content});
        }
      }
    }

    for (const tab of this.tabs) {
      tab.button.addEventListener('click', () => {
        this.activateAndUpdateAll(tab);
      });
    }
  }

  activateTab(tab) {
    tab.button.classList.add('is-active');
    tab.content.classList.add('is-active');
  }

  deactivateTab(tab) {
    tab.button.classList.remove('is-active');
    tab.content.classList.remove('is-active');
  }

  activateAndUpdateAll(tabToActivate) {
    for (const tab of this.tabs) {
      if (tab !== tabToActivate) {
        this.deactivateTab(tab);
      }
    }
    this.activateTab(tabToActivate);
  }
}

Initialized with

new Tabber(document.querySelector('.js-oop-tabber'));

FP Implementation

Tab Content 1
Tab Content 2
Tab Content 3
const getTabIndex = R.path(['dataset', 'tabIndex']);
const tabIndexEq = R.useWith(R.equals, [R.identity, getTabIndex]);
const activateElsWithTabIndex = R.curry((els, idx) => els.filter(tabIndexEq(idx)).map(addClass('is-active')));
const deactivateElsWithoutTabIndex = R.curry((els, idx) => els.filter(R.compose(R.not, tabIndexEq(idx))).map(removeClass('is-active')));

const initTabber = el => {
  const tabEls = [buttons, contents] = R.map(R.compose(Array.from, R.flip(qsAll)(el)))(['.js-Tabber-button', '.js-Tabber-body']);
  const toRun = R.ap([activateElsWithTabIndex, deactivateElsWithoutTabIndex], tabEls);
  const handler = R.compose(R.juxt(toRun), getTabIndex);
  Array.from(buttons).forEach(el => el.addEventListener('click', () => handler(el)));
}

Initialized with

initTabber(qs('.js-fp-tabber')(document));

Elm architecture

Tab Content 1
Test Nested HTML
Tab Content 3

Appendix

R refers to Ramda. I also wrote a couple general helper functions for the functional version.

const qsAll = R.invoker(1, 'querySelectorAll');
const qs = R.invoker(1, 'querySelector');
const addClass = R.curry((cls, el) => {
  el.classList.add(cls);
});
const removeClass = R.curry((cls, el) => {
  el.classList.remove(cls);
});