Группировка опций в выпадающих списках реализуется через механизм
optgroup, который позволяет логически объединять элементы
по категориям. В стандартной конфигурации библиотека отображает
заголовок группы как простой текстовый блок, однако в реальных
интерфейсах этого недостаточно: требуется визуальная иерархия,
дополнительная информация, иконки, счётчики элементов, состояния
активности и даже динамическая подгрузка содержимого.
Механизм кастомизации заголовков групп в Tom Select строится вокруг
переопределения рендер-функций и модификации структуры
optgroup. Основной точкой расширения выступает
render.optgroup_header, а также обработка данных в
optgroups и options.
Группы в Tom Select задаются через объектную структуру:
{
value: "fruits",
label: "Фрукты"
}
Или через расширенный вариант:
{
value: "fruits",
label: "Фрукты",
disabled: false
}
Каждая группа связывается с набором опций:
{
value: "apple",
text: "Яблоко",
optgroup: "fruits"
}
Ключевым моментом является то, что заголовок группы формируется не из
опции, а из объекта optgroup. Это позволяет полностью
отделить представление группы от её содержимого.
Основной способ изменения отображения групповых заголовков —
переопределение render.optgroup_header.
new TomSelect("#select", {
optgroups: [
{ value: "fruits", label: "Фрукты" },
{ value: "vegetables", label: "Овощи" }
],
options: [
{ value: "apple", text: "Яблоко", optgroup: "fruits" },
{ value: "carrot", text: "Морковь", optgroup: "vegetables" }
],
render: {
optgroup_header: function(data, escape) {
return `
<div class="group-header">
<span class="group-title">${escape(data.label)}</span>
</div>
`;
}
}
});
Здесь data содержит объект группы, а escape
обеспечивает защиту от XSS при вставке текста.
Структура optgroup может быть расширена произвольными
полями, которые затем используются в рендере:
optgroups: [
{
value: "fruits",
label: "Фрукты",
icon: "?",
count: 12
}
]
Рендер:
render: {
optgroup_header: function(data, escape) {
return `
<div class="group-header">
<span class="group-icon">${data.icon || ""}</span>
<span class="group-title">${escape(data.label)}</span>
<span class="group-count">${data.count ?? ""}</span>
</div>
`;
}
}
Такой подход позволяет превращать заголовок группы в полноценный UI-компонент.
Часто требуется динамически отображать количество элементов в группе. Это можно реализовать через предобработку данных:
const options = [
{ value: "apple", text: "Яблоко", optgroup: "fruits" },
{ value: "banana", text: "Банан", optgroup: "fruits" },
{ value: "carrot", text: "Морковь", optgroup: "vegetables" }
];
const optgroupCounts = options.reduce((acc, item) => {
acc[item.optgroup] = (acc[item.optgroup] || 0) + 1;
return acc;
}, {});
Далее данные передаются в optgroups:
optgroups: [
{ value: "fruits", label: "Фрукты", count: optgroupCounts["fruits"] },
{ value: "vegetables", label: "Овощи", count: optgroupCounts["vegetables"] }
]
И рендер остаётся ответственным только за отображение.
Заголовок может изменяться в зависимости от состояния группы: пустая, активная, заблокированная.
render: {
optgroup_header: function(data, escape) {
const isEmpty = data.count === 0;
return `
<div class="group-header ${isEmpty ? "is-empty" : ""}">
<span class="group-title">${escape(data.label)}</span>
${isEmpty ? "<span class='group-state'>нет элементов</span>" : ""}
</div>
`;
}
}
Такое форматирование часто используется в интерфейсах фильтрации, где группы могут динамически опустошаться.
Кастомизация заголовков групп обычно дополняется CSS:
.group-header {
display: flex;
justify-content: space-between;
padding: 8px 12px;
font-weight: 600;
border-bottom: 1px solid #eee;
}
.group-title {
color: #333;
}
.group-count {
font-size: 12px;
color: #888;
}
.group-header.is-empty {
opacity: 0.5;
}
Стилизация играет ключевую роль, поскольку заголовки групп часто становятся визуальными разделителями внутри dropdown-списка.
Добавление иконок повышает читаемость больших списков:
optgroups: [
{ value: "fruits", label: "Фрукты", iconClass: "icon-fruit" },
{ value: "vegetables", label: "Овощи", iconClass: "icon-veggie" }
]
Рендер:
render: {
optgroup_header: function(data, escape) {
return `
<div class="group-header">
<i class="${data.iconClass}"></i>
<span>${escape(data.label)}</span>
</div>
`;
}
}
Иконки позволяют быстро визуально различать группы без чтения текста.
В сложных интерфейсах группы могут формироваться на лету:
new TomSelect("#select", {
load: function(query, callback) {
fetch("/api/items?q=" + encodeURIComponent(query))
.then(res => res.json())
.then(data => {
const groups = {};
data.options.forEach(item => {
if (!groups[item.category]) {
groups[item.category] = {
value: item.category,
label: item.category_name,
count: 0
};
}
groups[item.category].count++;
});
this.settings.optgroups = Object.values(groups);
callback(data.options);
});
}
});
Заголовки групп в таком случае обновляются синхронно с результатами поиска.
Хотя заголовки групп по умолчанию не интерактивны, можно добавить поведение через DOM-обработчики:
render: {
optgroup_header: function(data, escape) {
return `
<div class="group-header js-group-header" data-group="${data.value}">
${escape(data.label)}
</div>
`;
}
}
Инициализация обработчика:
const control = new TomSelect("#select");
control.dropdown.addEventListener("click", function(e) {
const header = e.target.closest(".js-group-header");
if (!header) return;
const group = header.dataset.group;
console.log("Группа выбрана:", group);
});
Это позволяет реализовывать сворачивание/разворачивание групп или быстрый выбор всех элементов.
Группы в dropdown должны сохранять семантику для вспомогательных технологий. Важно поддерживать корректные роли:
role="group" для контейнера группыaria-label для заголовкаПример:
render: {
optgroup_header: function(data, escape) {
return `
<div role="group" aria-label="${escape(data.label)}" class="group-header">
${escape(data.label)}
</div>
`;
}
}
Это улучшает навигацию с клавиатуры и работу screen reader.
При большом количестве групп важно минимизировать перерасчёт DOM:
optgroup_headercount, label,
stateoptgroups без необходимостиОсобенно критично это при использовании load() с
удалёнными API, где частая перерисовка может приводить к мерцанию
интерфейса.
В продвинутых интерфейсах заголовок группы может включать несколько уровней информации:
render: {
optgroup_header: function(data, escape) {
return `
<div class="group-header">
<div class="group-main">
<span>${escape(data.label)}</span>
</div>
<div class="group-meta">
<span>${data.count} элементов</span>
<span>${data.description || ""}</span>
</div>
</div>
`;
}
}
Такой подход превращает заголовок в мини-карточку, позволяя размещать контекст без перегрузки списка опций.
Порядок групп можно контролировать не только через массив
optgroups, но и через сортировку перед передачей в Tom
Select:
optgroups.sort((a, b) => a.priority - b.priority);
Заголовки в этом случае автоматически следуют заданной логике приоритета.
При использовании встроенного поиска важно учитывать, что заголовок группы может отражать состояние фильтра:
Пример обновления:
optgroups.forEach(g => {
g.count = options.filter(o => o.optgroup === g.value && o.visible).length;
});
Это делает заголовки динамическими и контекстно-зависимыми.