Отображение счетчика выбранных

При использовании режима multiple в Tom Select интерфейс нередко сталкивается с проблемой перегруженности. Если пользователь выбирает десятки значений, поле начинает занимать слишком много места, ухудшается читаемость формы, появляются проблемы с адаптивностью и производительностью интерфейса.

Счетчик выбранных элементов решает эту проблему:

  • уменьшает визуальный шум;
  • сохраняет компактность компонента;
  • показывает количество выбранных значений;
  • улучшает UX при большом количестве тегов;
  • упрощает работу на мобильных устройствах.

Наиболее распространённые варианты отображения:

  • Выбрано: 5
  • 5 элементов
  • Selected (5)
  • +10
  • Товары: 24
  • 3 из 20

Базовый принцип реализации

Tom Select по умолчанию отображает каждый выбранный элемент как отдельный tag/item внутри контейнера .ts-control.

Пример стандартного поведения:

<input id="tags" multiple>
new TomSelect('#tags', {
    plugins: ['remove_button']
});

После выбора значений HTML внутри компонента будет выглядеть примерно так:

<div class="ts-control multi">
    <div data-value="js" class="item">JavaScript</div>
    <div data-value="css" class="item">CSS</div>
    <div data-value="html" class="item">HTML</div>

    <input type="text">
</div>

Задача счетчика — заменить отображение множества элементов на компактный индикатор количества.


Получение количества выбранных элементов

Tom Select хранит выбранные значения в массиве items.

Количество можно получить так:

const select = new TomSelect('#tags');

console.log(select.items.length);

Либо через API:

const count = select.getValue().length;

Для multiple оба варианта подходят.


Создание простого счетчика

HTML

<select id="skills" multiple>
    <option value="js">JavaScript</option>
    <option value="css">CSS</option>
    <option value="html">HTML</option>
    <option value="node">Node.js</option>
</select>

<div id="counter"></div>

Инициализация

const counter = document.querySelector('#counter');

const select = new TomSelect('#skills', {
    plugins: ['remove_button'],

    onChange(values) {
        counter.textContent = `Выбрано: ${values.length}`;
    }
});

Результат

После каждого изменения:

Выбрано: 3

Автоматическое обновление при удалении

Событие onChange вызывается:

  • при выборе;
  • при удалении;
  • при очистке;
  • при программном изменении значений.

Поэтому отдельная обработка удаления не требуется.


Инициализация счетчика при загрузке

Если значения уже выбраны заранее:

<option value="js" selected>JavaScript</option>
<option value="css" selected>CSS</option>

необходимо обновить счетчик после создания экземпляра.

const counter = document.querySelector('#counter');

const select = new TomSelect('#skills', {
    onInitialize() {
        counter.textContent =
            `Выбрано: ${this.items.length}`;
    },

    onChange(values) {
        counter.textContent =
            `Выбрано: ${values.length}`;
    }
});

Универсальная функция обновления

Чтобы избежать дублирования:

function updateCounter(instance, element) {
    element.textContent =
        `Выбрано: ${instance.items.length}`;
}

Использование:

const counter = document.querySelector('#counter');

const select = new TomSelect('#skills', {

    onInitialize() {
        updateCounter(this, counter);
    },

    onChange() {
        updateCounter(this, counter);
    }
});

Скрытие тегов и отображение только счетчика

Это наиболее популярный сценарий.

CSS

.ts-control .item {
    display: none;
}

Теперь выбранные элементы не отображаются.


Добавление счетчика внутрь компонента

const select = new TomSelect('#skills', {

    onInitialize() {

        this.counter = document.createElement('div');
        this.counter.className = 'selected-counter';

        this.control.appendChild(this.counter);

        updateCounter(this);
    },

    onChange() {
        updateCounter(this);
    }
});

function updateCounter(instance) {

    const count = instance.items.length;

    instance.counter.textContent =
        `Выбрано: ${count}`;
}

Стилизация

.selected-counter {
    padding: 4px 8px;
    background: #f3f3f3;
    border-radius: 4px;
    font-size: 14px;
    color: #444;
}

Отображение первых элементов и счетчика остальных

Распространённый UX-подход:

JavaScript, CSS +12

Логика

  • первые 2–3 элемента отображаются;
  • остальные скрываются;
  • показывается остаток.

Реализация

const LIMIT = 2;

const select = new TomSelect('#skills', {

    onInitialize() {
        updateView(this);
    },

    onChange() {
        updateView(this);
    }
});

function updateView(instance) {

    const items =
        instance.control.querySelectorAll('.item');

    items.forEach((item, index) => {

        if(index < LIMIT) {
            item.style.display = '';
        } else {
            item.style.display = 'none';
        }
    });

    const hiddenCount =
        instance.items.length - LIMIT;

    let counter =
        instance.control.querySelector('.more-count');

    if(!counter) {

        counter = document.createElement('span');

        counter.className = 'more-count';

        instance.control.appendChild(counter);
    }

    if(hiddenCount > 0) {

        counter.textContent =
            `+${hiddenCount}`;

        counter.style.display = '';

    } else {

        counter.style.display = 'none';
    }
}

Динамический текст счетчика

Склонение слов

Для русского языка часто требуется правильное склонение.

Пример

1 элемент
2 элемента
5 элементов

Функция склонения

function plural(count, one, few, many) {

    const mod10 = count % 10;
    const mod100 = count % 100;

    if(mod100 >= 11 && mod100 <= 19) {
        return many;
    }

    if(mod10 === 1) {
        return one;
    }

    if(mod10 >= 2 && mod10 <= 4) {
        return few;
    }

    return many;
}

Использование

function updateCounter(instance) {

    const count = instance.items.length;

    const word = plural(
        count,
        'элемент',
        'элемента',
        'элементов'
    );

    instance.counter.textContent =
        `${count} ${word}`;
}

Счетчик с лимитом выбора

Иногда важно одновременно показывать:

  • количество выбранных;
  • максимальный лимит.

Пример

3 / 10

Реализация

const MAX_ITEMS = 10;

const select = new TomSelect('#skills', {

    maxItems: MAX_ITEMS,

    onInitialize() {

        this.counter =
            document.querySelector('#counter');

        updateCounter(this);
    },

    onChange() {
        updateCounter(this);
    }
});

function updateCounter(instance) {

    instance.counter.textContent =
        `${instance.items.length} / ${MAX_ITEMS}`;
}

Счетчик внутри placeholder

Иногда количество выбранных отображается прямо в поле ввода.


Пример

Выбрано 7 элементов

Реализация

const select = new TomSelect('#skills', {

    onInitialize() {
        updatePlaceholder(this);
    },

    onChange() {
        updatePlaceholder(this);
    }
});

function updatePlaceholder(instance) {

    const count = instance.items.length;

    instance.control_input.placeholder =
        `Выбрано ${count} элементов`;
}

Полное скрытие input

Если поиск не нужен:

controlInput: null

Пример:

new TomSelect('#skills', {
    controlInput: null
});

В таком случае счетчик становится главным элементом интерфейса.


Использование render для кастомного счетчика

Tom Select позволяет переопределять шаблоны.


Кастомный item

new TomSelect('#skills', {

    render: {

        item(data, escape) {

            return `
                <div class="item">
                    ${escape(data.text)}
                </div>
            `;
        }
    }
});

Генерация отдельного блока счетчика

function createCounter(instance) {

    const el = document.createElement('div');

    el.className = 'ts-counter';

    instance.control.appendChild(el);

    return el;
}

Использование плагина для счетчика

Для крупных проектов лучше вынести логику в plugin.


Создание plugin

TomSelect.define('selection_counter', function(options) {

    const self = this;

    self.on('initialize', () => {

        self.counter =
            document.createElement('div');

        self.counter.className =
            'selection-counter';

        self.control.appendChild(self.counter);

        update();
    });

    self.on('change', update);

    function update() {

        const count = self.items.length;

        self.counter.textContent =
            `Выбрано: ${count}`;
    }
});

Подключение

new TomSelect('#skills', {
    plugins: ['selection_counter']
});

Передача настроек в plugin

TomSelect.define('selection_counter', function(options) {

    const self = this;

    const label =
        options.label || 'Выбрано';

    self.on('initialize', () => {

        self.counter =
            document.createElement('div');

        self.control.appendChild(self.counter);

        update();
    });

    self.on('change', update);

    function update() {

        self.counter.textContent =
            `${label}: ${self.items.length}`;
    }
});

Использование

new TomSelect('#skills', {

    plugins: {
        selection_counter: {
            label: 'Теги'
        }
    }
});

Счетчик и remote data

При загрузке данных с сервера логика не меняется.

new TomSelect('#users', {

    valueField: 'id',
    labelField: 'name',
    searchField: 'name',

    load(query, callback) {

        fetch('/api/users?q=' + query)
            .then(r => r.json())
            .then(callback);
    },

    onChange(values) {

        console.log(values.length);
    }
});

Количество выбранных элементов всегда хранится локально в items.


Производительность при большом количестве элементов

При выборе сотен значений отображение всех тегов становится дорогим:

  • растёт DOM;
  • увеличивается количество repaint;
  • замедляется layout;
  • ухудшается responsiveness.

Счетчик помогает избежать этих проблем.


Оптимизация через скрытие DOM-элементов

Вместо удаления:

item.remove();

лучше использовать:

item.style.display = 'none';

Это уменьшает количество операций перестроения DOM.


Полная замена отображения выбранных элементов

Иногда требуется полностью отключить теги.


CSS

.ts-control .item {
    display: none !important;
}

Отображение только количества

function updateCounter(instance) {

    instance.counter.textContent =
        `${instance.items.length} selected`;
}

Счетчик с иконкой

<div class="selection-counter">
    <span class="icon">✓</span>
    <span class="count">0</span>
</div>

Обновление

instance.counter.querySelector('.count')
    .textContent = instance.items.length;

Счетчик в dropdown

Иногда количество выбранных элементов отображается внутри списка.


Пример

Выбрано: 12
----------------
JavaScript
CSS
HTML

Добавление header

new TomSelect('#skills', {

    render: {

        dropdown() {

            return `
                <div>
                    <div class="dropdown-header">
                        Выбрано: 0
                    </div>

                    <div class="dropdown-content"></div>
                </div>
            `;
        }
    }
});

Обновление header

const header =
    instance.dropdown.querySelector(
        '.dropdown-header'
    );

header.textContent =
    `Выбрано: ${instance.items.length}`;

Работа с событиями Tom Select

Для счетчика особенно полезны:

Событие Назначение
initialize создание счетчика
change обновление
item_add реакция на добавление
item_remove реакция на удаление
clear очистка

Использование item_add и item_remove

Иногда удобнее разделить обработчики.

new TomSelect('#skills', {

    onItemAdd() {
        updateCounter(this);
    },

    onItemRemove() {
        updateCounter(this);
    }
});

Анимация счетчика

.selection-counter {
    transition: all .2s ease;
}

Эффект обновления

function animate(counter) {

    counter.classList.add('updated');

    setTimeout(() => {
        counter.classList.remove('updated');
    }, 200);
}

Интеграция с Bootstrap

<div class="badge bg-primary" id="counter">
    0
</div>

Обновление

counter.textContent =
    select.items.length;

Интеграция с Tailwind

<div
    id="counter"
    class="px-2 py-1 rounded bg-blue-100 text-blue-700 text-sm">
</div>

Использование MutationObserver

Если DOM изменяется сторонними скриптами:

const observer =
    new MutationObserver(() => {

        updateCounter(select);
    });

observer.observe(select.control, {
    childList: true
});

Типичные ошибки

Отсутствие обновления при initialize

Ошибка:

onChange() {
    updateCounter(this);
}

При заранее выбранных значениях счетчик будет пустым до первого изменения.


Использование getValue без multiple

Для одиночного select:

getValue()

вернёт строку, а не массив.


Удаление DOM-элементов вместо скрытия

Плохой вариант:

item.remove();

Tom Select продолжает хранить внутренние ссылки на элементы.


Рекомендуемая архитектура

Для крупных проектов наиболее устойчивый подход:

  • отдельный plugin;
  • отдельная функция рендера;
  • CSS-класс для скрытия тегов;
  • обновление через события change;
  • минимальные DOM-операции;
  • хранение счетчика внутри control.

Такая архитектура обеспечивает:

  • переиспользуемость;
  • производительность;
  • простую поддержку;
  • единое поведение во всех формах;
  • минимальное вмешательство в ядро Tom Select.