Предисловие


Какое-то время назад я стал постепенно отказываться от jQuery в пользу нативного javascript. Это связано с тем, что поддержка старых браузеров перестала быть приоритетной и на первое место вышла скорость загрузки страницы. Я не смог найти минималистичный модуль вкладок с простой html разметкой – поэтому решил написать свой.

Демо, Исходный код на Github

HTML разметка


<div class="tabs">
	<div class="tabs__toggle tabs__toggle_active">Вкладка 1</div>
	<div class="tabs__toggle">Вкладка 2</div>
	<div class="tabs__tab">
		Содержимое первой вкладки
	</div>
	<div class="tabs__tab">
		Содержимое второй вкладки
	</div>
</div>

Если на одной странице нужно разместить несколько групп вкладок нужно просто разделить их в разные блоки '.tabs'. Расположение внутренних блоков влияет только на порядок их вывода. Вкладке по умолчанию следует добавить класс 'tabs__toggle_active'.

Класс закладки


class Tab {
    constructor (tabs, toggle, tab) {
        this.tabs = tabs;
        this.toggle = toggle;
        this.tab = tab;
        this.init();
    }
    init () {
        if (this.toggle.classList.contains('tabs__toggle_active')) {
            this.open();
        } else {
            this.close();
        }

        this.toggle.addEventListener('click', () => {
            this.open();
        });
    }
    open () {
        if (this.tabs.active === this) {
            // already open
            return;
        }
        if (this.tabs.active) {
            this.tabs.active.close();
        }
        this.tabs.active = this;
        this.tab.style.display = 'block';
        this.toggle.classList.add('tabs__toggle_active');
    }
    close () {
        this.tab.style.display = 'none';
        this.toggle.classList.remove('tabs__toggle_active');
    }
}

Конструктор принимает родительский класс группы вкладок, DOM элемент кнопки по которой открывается вкладка и DOM вкладки, к которой относится данная кнопка.

Функция init проверяет, является ли эта вкладка открытой по умолчанию и добавляет событие открытия по клику.

Функция open закрывает открытую вкладку при ее наличии и устанавливает ссылку на собственный экземпляр класса в свойство 'active' родительского класса. Так же проставляет активный класс для стилизации кнопки и свойство 'display' вкладки.

Функция close убирает активный класс с кнопки и скрывает вкладку.

Класс группы вкладок


export class Tabs {
    constructor (container) {
        this.container = container;
        this.init();
    }
    init () {
        this.toggles = this.container.querySelectorAll('.tabs__toggle');
        this.tabs = this.container.querySelectorAll('.tabs__tab');
        if (!this.isEverythingOk()) {
            return;
        }

        for (let index = 0; index < this.toggles.length; index++) {
            new Tab (this, this.toggles[index], this.tabs[index]);
        }
    }
    isEverythingOk () {
        if (this.toggles.length !== this.tabs.length) {
            console.warn('Tabs toggles and tabs amounts are not matching');
            return false;
        } else if (this.toggles.length === 0) {
            console.warn('There\'s no toggles for tabs');
            return false;
        } else if (this.tabs.length === 0) {
            console.warn('There\'s no content tabs');
            return false;
        }
        return true;
    }
}

Конструктор принимает DOM объект группы вкладок (в нашем случае .tabs).

Функция init проходит циклом по всем кнопкам и создает экземпляры класса Tab группируя по принципу «первая кнопка к первой вкладке».

Функция isEverythingOk проверяет соответствие количества вкладок количеству кнопок и их наличие, в противном случае выбрасывает предупреждение в консоль для более удобного поиска ошибок.

Функция инициализации


export default function initTabs(selector) {
    for (let container of document.querySelectorAll(selector)) {
        new Tabs(container);
    }
}

Функция предназначена для тех, кто не хочет разбираться с принципами работы с DOM или же просто для удобства. Создает экземпляры класса Tabs.

Пример с использованием функции инициализации


import initTabs from 'future-tabs';
initTabs('.tabs');


Пример работы напрямую с классом


import {Tabs} from 'future-tabs';
const container = document.querySelector('.tabs');
const tabs = new Tabs(container);


В планах сделать выборку внутренних блоков в зависимости от селектора, дописывая названия элементов следуя методологии _bem.

Github

Спасибо за внимание!

P.S. Сделал опцию названия блока по _bem. Подробности в документации на гитхабе.

Комментарии (13)


  1. stcherenkov
    26.08.2015 15:21

    Если для вас неактуальна поддержка старых браузеров, то есть решение на чистом HTML+CSS: css-tricks.com/functional-css-tabs-revisited


    1. prog666
      26.08.2015 15:31
      +2

      html разметка не такая удобная и универсальная, иногда переключатели и вкладки должны находится в отдельных контейнерах. Подобные решения на HTML+CSS нужны скорее для демонстрации возможностей чем для практического использования.


      1. romy4
        26.08.2015 20:11

        В приведённом выше решении расположить табы и вкладки в отдельных контейнерах как раз не составит труда. Label можно вынести куда угодно, а скрытый radio и контейнер останутся на одном уровне.


        1. prog666
          26.08.2015 20:41

          Согласен, но еще остается необходимость прописывать для каждой вкладки уникальные названия.


  1. xamd
    26.08.2015 17:55

    Ну, признаться, Вы кроме классов, блочных переменных и деструкторов ничего не используете. Возможно, статья была бы более информативна, если бы вы параллельно рассмотрели реализацию на ES5 и более очевидно указали бы на выигрыш, который программист получает, используя новый синтаксис.

    Но пример выбран, на мой взгляд, не самым удачным образом.


    1. prog666
      26.08.2015 17:59

      Это моя первая статья, как раз надеялся получить критику.


    1. prog666
      26.08.2015 18:08

      Но смысл я уловил, спасибо. Следующая статья будет более обучающей.


  1. romy4
    26.08.2015 20:16

    Хотел уточнить, использование ключевых слов class и export default — это из какого javascript/Ecmascript?


    1. prog666
      26.08.2015 20:40

      да, в заголовке указано. es2015 или как его называли раньше es6


      1. romy4
        26.08.2015 20:53

        Спасибо :) Я как-то на автомате пропустил текст аббревиатур в заголовке


        1. prog666
          26.08.2015 21:33

          Не за что, могу посоветовать почитать про babel


  1. feedborg
    27.08.2015 00:42

    Это связано с тем, что поддержка старых браузеров перестала быть приоритетной и на первое место вышла скорость загрузки страницы

    Старых — это каких? И насколько секунд сократилось время загрузки? Сколько было?

    Ну и я сильно сомневаюсь, что синтаксический сахар, под названием «class» хоть что-то ускорил. Без тестов мои слова ничего не стоят, впрочем, как и ваш топик.


    1. prog666
      27.08.2015 00:53
      +1

      Насколько секунд сократилось время загрузки?

      Дело же не в es6, дело в отсутствии необходимости загружать jquery, предыдущий мой такой модуль был плагином для него (jquery).

      Старых — это каких?

      Старых это тех которые не поддерживают такие вещи как document.querySelector и classList, хотя для последнего есть полифил ссылка на который есть в readme на github, для querySelector возможно тоже есть полифил.

      Ну и я сильно сомневаюсь, что синтаксический сахар, под названием «class» хоть что-то ускорил.

      По поводу скорости работы — дело в работе с DOM напрямую а не через оболочку jquery. Весь этот синтаксический сахар конечно потом транслируется в обычный es5 с помощью babel, там даже в репозитории есть уже готовый es5 файл, но подключать его все равно нужно с помощью browserify или подобной библеотеки для работы с commonjs модулями (что-то слышал про webpack но не пробовал пока что)