Группы в Tom Select используются для логического разделения элементов внутри выпадающего списка. Они особенно полезны при работе с большими наборами данных: категориями товаров, странами и городами, ролями пользователей, тегами, разделами документации и любыми структурированными коллекциями.
Базовая структура групп строится через свойства:
optgroupFieldoptgroupsoptgroupLabelFieldoptgroupValueFieldПример конфигурации:
<select id="frameworks" multiple>
<option value="vue" data-group="frontend">Vue</option>
<option value="react" data-group="frontend">React</option>
<option value="laravel" data-group="backend">Laravel</option>
<option value="django" data-group="backend">Django</option>
</select>
new TomSelect('#frameworks', {
optgroupField: 'group',
optgroups: [
{ value: 'frontend', label: 'Frontend' },
{ value: 'backend', label: 'Backend' }
]
});
После инициализации список будет отображать элементы по секциям.
При большом количестве элементов группы могут занимать значительную часть интерфейса. Сворачивание позволяет:
Типичные сценарии:
| Сценарий | Пример |
|---|---|
| Каталог товаров | Электроника, одежда, книги |
| IDE/редактор | Сниппеты по категориям |
| CRM | Группы клиентов |
| География | Континенты → страны |
| Управление доступом | Роли и разрешения |
Tom Select не содержит штатной системы сворачивания групп. Библиотека предоставляет:
Сворачивание реализуется вручную через:
Это даёт полную гибкость.
После рендеринга Tom Select создаёт примерно такую структуру:
<div class="optgroup">
<div class="optgroup-header">Frontend</div>
<div data-selectable class="option">Vue</div>
<div data-selectable class="option">React</div>
</div>
Ключевые элементы:
| Элемент | Назначение |
|---|---|
.optgroup |
контейнер группы |
.optgroup-header |
заголовок |
.option |
элемент списка |
Сворачивание обычно реализуется через скрытие
.option.
Наиболее распространённый способ — добавление класса:
.optgroup.collapsed .option {
display: none;
}
Теперь достаточно переключать класс collapsed.
const select = new TomSelect('#frameworks', {
onInitialize() {
this.dropdown.addEventListener('click', (event) => {
const header = event.target.closest('.optgroup-header');
if (!header) return;
const group = header.parentElement;
group.classList.toggle('collapsed');
});
}
});
Что происходит:
.optgroup-header..optgroup.collapsed.Стандартный рендер заголовков часто недостаточен. Обычно добавляют:
Пример:
new TomSelect('#frameworks', {
render: {
optgroup_header(data, escape) {
return `
<div class="optgroup-header">
<span class="toggle-icon">▶</span>
<span class="group-title">
${escape(data.label)}
</span>
</div>
`;
}
}
});
Свойство:
display: none;
не анимируется.
Для плавного эффекта используют:
max-height;opacity;overflow..optgroup-content {
max-height: 500px;
overflow: hidden;
transition:
max-height 0.3s ease,
opacity 0.3s ease;
opacity: 1;
}
.optgroup.collapsed .optgroup-content {
max-height: 0;
opacity: 0;
}
Для анимации потребуется дополнительный контейнер:
<div class="optgroup">
<div class="optgroup-header">
Frontend
</div>
<div class="optgroup-content">
<div class="option">Vue</div>
<div class="option">React</div>
</div>
</div>
Tom Select сам не создаёт .optgroup-content, поэтому
структуру изменяют вручную.
onInitialize() {
const groups = this.dropdown.querySelectorAll('.optgroup');
groups.forEach(group => {
const options = [
...group.querySelectorAll('.option')
];
const wrapper = document.createElement('div');
wrapper.className = 'optgroup-content';
options.forEach(option => {
wrapper.appendChild(option);
});
group.appendChild(wrapper);
});
}
При повторном открытии dropdown состояние может сбрасываться. Поэтому удобно хранить информацию отдельно.
const collapsedGroups = {};
this.dropdown.addEventListener('click', (event) => {
const header = event.target.closest('.optgroup-header');
if (!header) return;
const group = header.parentElement;
const groupName = group.dataset.group;
group.classList.toggle('collapsed');
collapsedGroups[groupName] =
group.classList.contains('collapsed');
});
groups.forEach(group => {
const name = group.dataset.group;
if (collapsedGroups[name]) {
group.classList.add('collapsed');
}
});
Для постоянного хранения:
localStorage.setItem(
'collapsed-groups',
JSON.stringify(collapsedGroups)
);
Загрузка:
const collapsedGroups =
JSON.parse(
localStorage.getItem('collapsed-groups')
) || {};
.toggle-icon {
display: inline-block;
transition: transform 0.2s ease;
}
.optgroup.collapsed .toggle-icon {
transform: rotate(-90deg);
}
Иногда после поиска группа не содержит видимых элементов.
function updateEmptyGroups(select) {
const groups =
select.dropdown.querySelectorAll('.optgroup');
groups.forEach(group => {
const visibleOptions =
group.querySelectorAll('.option:not(.hidden)');
if (!visibleOptions.length) {
group.style.display = 'none';
} else {
group.style.display = '';
}
});
}
Некоторые интерфейсы автоматически раскрывают группы при вводе поиска.
onType(str) {
const groups =
this.dropdown.querySelectorAll('.optgroup');
groups.forEach(group => {
if (str.length) {
group.classList.remove('collapsed');
}
});
}
Более продвинутый вариант:
function expandMatchedGroups(select) {
const groups =
select.dropdown.querySelectorAll('.optgroup');
groups.forEach(group => {
const visible =
group.querySelector('.option:not(.hidden)');
if (visible) {
group.classList.remove('collapsed');
}
});
}
Иногда требуется раскрывать только одну группу одновременно.
this.dropdown.addEventListener('click', (event) => {
const header =
event.target.closest('.optgroup-header');
if (!header) return;
const currentGroup =
header.parentElement;
const groups =
this.dropdown.querySelectorAll('.optgroup');
groups.forEach(group => {
if (group !== currentGroup) {
group.classList.add('collapsed');
}
});
currentGroup.classList.toggle('collapsed');
});
Можно сразу скрывать определённые секции.
onInitialize() {
const groups =
this.dropdown.querySelectorAll('.optgroup');
groups.forEach(group => {
if (group.dataset.group === 'backend') {
group.classList.add('collapsed');
}
});
}
Удобно создать методы управления.
function collapseGroup(name) {
const group = document.querySelector(
`.optgroup[data-group="${name}"]`
);
if (!group) return;
group.classList.add('collapsed');
}
function expandGroup(name) {
const group = document.querySelector(
`.optgroup[data-group="${name}"]`
);
if (!group) return;
group.classList.remove('collapsed');
}
function toggleGroup(name) {
const group = document.querySelector(
`.optgroup[data-group="${name}"]`
);
if (!group) return;
group.classList.toggle('collapsed');
}
function collapseAll(select) {
select.dropdown
.querySelectorAll('.optgroup')
.forEach(group => {
group.classList.add('collapsed');
});
}
function expandAll(select) {
select.dropdown
.querySelectorAll('.optgroup')
.forEach(group => {
group.classList.remove('collapsed');
});
}
При использовании load() группы могут появляться
позже.
load(query, callback) {
fetch('/api/tags')
.then(r => r.json())
.then(data => {
callback(data);
requestAnimationFrame(() => {
initializeGroups(this);
});
});
}
При сложной системе сворачивания удобно создавать собственный plugin.
TomSelect.define('collapsible_groups', function() {
const self = this;
self.on('initialize', () => {
self.dropdown.addEventListener(
'click',
(event) => {
const header =
event.target.closest('.optgroup-header');
if (!header) return;
const group =
header.parentElement;
group.classList.toggle('collapsed');
}
);
});
});
Использование:
new TomSelect('#frameworks', {
plugins: ['collapsible_groups']
});
group.animate([
{
opacity: 0
},
{
opacity: 1
}
], {
duration: 200
});
При изменении dropdown можно автоматически обрабатывать новые группы.
const observer = new MutationObserver(() => {
initializeGroups(select);
});
observer.observe(select.dropdown, {
childList: true,
subtree: true
});
Иногда клик по заголовку вызывает закрытие списка.
header.addEventListener('mousedown', (event) => {
event.preventDefault();
});
Для accessibility желательно поддерживать:
header.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
group.classList.toggle('collapsed');
}
if (event.key === 'ArrowLeft') {
group.classList.add('collapsed');
}
if (event.key === 'ArrowRight') {
group.classList.remove('collapsed');
}
});
Рекомендуется использовать:
aria-expanded="true"
и:
role="button"
tabindex="0"
const expanded =
!group.classList.contains('collapsed');
header.setAttribute(
'aria-expanded',
expanded
);
При большом количестве групп важно:
Tom Select может пересоздавать dropdown.
Решение:
select.on('dropdown_open', () => {
initializeGroups(select);
});
Некоторые разработчики изменяют внутренний HTML слишком агрессивно, из-за чего ломается навигация.
Безопаснее:
.option.Если используется кастомная виртуализация списка:
const collapsed = {};
new TomSelect('#frameworks', {
onInitialize() {
const select = this;
initialize();
function initialize() {
const groups =
select.dropdown.querySelectorAll('.optgroup');
groups.forEach(group => {
const header =
group.querySelector('.optgroup-header');
if (!header) return;
const name =
group.dataset.group;
if (collapsed[name]) {
group.classList.add('collapsed');
}
header.addEventListener('click', () => {
group.classList.toggle('collapsed');
collapsed[name] =
group.classList.contains('collapsed');
});
});
}
select.on('dropdown_open', initialize);
}
});
.optgroup-header {
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-weight: bold;
user-select: none;
}
.optgroup.collapsed .option {
display: none;
}
.toggle-icon {
transition: transform 0.2s ease;
}
.optgroup.collapsed .toggle-icon {
transform: rotate(-90deg);
}