При использовании режима multiple в Tom Select интерфейс
нередко сталкивается с проблемой перегруженности. Если пользователь
выбирает десятки значений, поле начинает занимать слишком много места,
ухудшается читаемость формы, появляются проблемы с адаптивностью и
производительностью интерфейса.
Счетчик выбранных элементов решает эту проблему:
Наиболее распространённые варианты отображения:
Выбрано: 55 элементовSelected (5)+10Товары: 243 из 20Tom 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 оба варианта подходят.
<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);
}
});
Это наиболее популярный сценарий.
.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
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}`;
}
Иногда количество выбранных отображается прямо в поле ввода.
Выбрано 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} элементов`;
}
Если поиск не нужен:
controlInput: null
Пример:
new TomSelect('#skills', {
controlInput: null
});
В таком случае счетчик становится главным элементом интерфейса.
Tom Select позволяет переопределять шаблоны.
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.
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']
});
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: 'Теги'
}
}
});
При загрузке данных с сервера логика не меняется.
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.
При выборе сотен значений отображение всех тегов становится дорогим:
Счетчик помогает избежать этих проблем.
Вместо удаления:
item.remove();
лучше использовать:
item.style.display = 'none';
Это уменьшает количество операций перестроения DOM.
Иногда требуется полностью отключить теги.
.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;
Иногда количество выбранных элементов отображается внутри списка.
Выбрано: 12
----------------
JavaScript
CSS
HTML
new TomSelect('#skills', {
render: {
dropdown() {
return `
<div>
<div class="dropdown-header">
Выбрано: 0
</div>
<div class="dropdown-content"></div>
</div>
`;
}
}
});
const header =
instance.dropdown.querySelector(
'.dropdown-header'
);
header.textContent =
`Выбрано: ${instance.items.length}`;
Для счетчика особенно полезны:
| Событие | Назначение |
|---|---|
| initialize | создание счетчика |
| change | обновление |
| item_add | реакция на добавление |
| item_remove | реакция на удаление |
| clear | очистка |
Иногда удобнее разделить обработчики.
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);
}
<div class="badge bg-primary" id="counter">
0
</div>
counter.textContent =
select.items.length;
<div
id="counter"
class="px-2 py-1 rounded bg-blue-100 text-blue-700 text-sm">
</div>
Если DOM изменяется сторонними скриптами:
const observer =
new MutationObserver(() => {
updateCounter(select);
});
observer.observe(select.control, {
childList: true
});
Ошибка:
onChange() {
updateCounter(this);
}
При заранее выбранных значениях счетчик будет пустым до первого изменения.
Для одиночного select:
getValue()
вернёт строку, а не массив.
Плохой вариант:
item.remove();
Tom Select продолжает хранить внутренние ссылки на элементы.
Для крупных проектов наиболее устойчивый подход:
change;control.Такая архитектура обеспечивает: