Вложенные группы в Tom Sel ect опираются на базовый механизм optgroup, который в стандартной реализации ограничен одним уровнем иерархии. При этом сама архитектура библиотеки позволяет реализовать многоуровневую группировку через преобразование данных, кастомные шаблоны рендера и управление структурой источника опций.
В основе групп лежит поле optgroup, которое связывает
каждую опцию с конкретной группой. Структура данных обычно выглядит
следующим образом:
optionsoptgroupsoptgroupFieldnew TomSelect("#select", {
options: [
{ value: "js", text: "JavaScript", group: "frontend" },
{ value: "css", text: "CSS", group: "frontend" },
{ value: "node", text: "Node.js", group: "backend" }
],
optgroups: [
{ value: "frontend", label: "Frontend" },
{ value: "backend", label: "Backend" }
],
optgroupField: "group"
});
На этом уровне группа является плоской сущностью без вложенности. Каждый элемент принадлежит ровно одному optgroup.
Стандартная модель группировки в Tom Select не поддерживает вложенные группы напрямую. Следующие ограничения фиксированы:
Это означает, что любая иерархия должна быть преобразована до передачи в компонент.
Основной подход к реализации вложенных групп — преобразование дерева в линейный список с кодированием пути.
const options = [
{ value: "react", text: "React", path: "Frontend/Frameworks" },
{ value: "vue", text: "Vue", path: "Frontend/Frameworks" },
{ value: "css", text: "CSS", path: "Frontend/Styling" },
{ value: "node", text: "Node.js", path: "Backend/Runtime" }
];
Здесь поле path заменяет вложенную структуру.
Перед инициализацией данные преобразуются в набор optgroups:
function buildOptgroups(items) {
const map = new Map();
items.forEach(item => {
const parts = item.path.split("/");
let currentPath = "";
parts.forEach(part => {
currentPath = currentPath ? `${currentPath}/${part}` : part;
if (!map.has(currentPath)) {
map.set(currentPath, {
value: currentPath,
label: part,
parent: currentPath.includes("/")
? currentPath.substring(0, currentPath.lastIndexOf("/"))
: null
});
}
});
});
return Array.fr om(map.values());
}
Результат — плоский список групп, где каждая группа знает своего
логического родителя через поле parent, хотя Tom Select его
не использует напрямую.
Tom Select позволяет переопределять отображение групп через
render.optgroup_header. Именно этот механизм используется
для визуальной имитации вложенности.
new TomSelect("#select", {
options,
optgroups: buildOptgroups(options),
optgroupField: "path",
render: {
optgroup_header: function(data, escape) {
const depth = data.value.split("/").length - 1;
return `
<div class="optgroup-header depth-${depth}">
${escape(data.label)}
</div>
`;
}
}
});
Глубина определяется количеством сегментов пути:
Frontend → уровень 0Frontend/Frameworks → уровень 1Frontend/Frameworks/Advanced → уровень 2CSS-оформление:
.optgroup-header {
font-weight: bold;
}
.depth-1 {
padding-left: 12px;
}
.depth-2 {
padding-left: 24px;
}
Таким образом достигается визуальная иерархия без изменения DOM-структуры Tom Select.
Другой подход — отказ от optgroup как логической структуры и использование специальных «разделителей» внутри списка.
options: [
{ value: "frontend", text: "Frontend", disabled: true },
{ value: "react", text: "React", group: "frontend" },
{ value: "vue", text: "Vue", group: "frontend" },
{ value: "backend", text: "Backend", disabled: true },
{ value: "node", text: "Node.js", group: "backend" }
]
В этом случае группы становятся визуальными маркерами, а не структурными сущностями.
При работе с API часто приходит истинно древовидная структура:
const tree = {
label: "Frontend",
children: [
{
label: "Frameworks",
children: [
{ value: "react", text: "React" },
{ value: "vue", text: "Vue" }
]
}
]
};
function flattenTree(node, path = "", groups = [], options = []) {
const currentPath = path ? `${path}/${node.label}` : node.label;
if (node.value) {
options.push({
value: node.value,
text: node.text,
group: path
});
}
if (node.children) {
groups.push({
value: currentPath,
label: node.label
});
node.children.forEach(child =>
flattenTree(child, currentPath, groups, options)
);
}
return { groups, options };
}
Результат разделяется на:
groups — плоский список optgroupoptions — элементы с привязкой к группамФильтрация в Tom Select не учитывает иерархию групп как отдельную структуру. Поиск работает по:
textvaluesearchFieldПри вложенной структуре важно учитывать:
function filterEmptyGroups(optgroups, options) {
const used = new Set(options.map(o => o.group));
return optgroups.filter(g => used.has(g.value));
}
Порядок групп становится критическим при наличии нескольких уровней.
optgroups.sort((a, b) => {
return a.value.localeCompare(b.value);
});
optgroups.sort((a, b) => {
const depthA = a.value.split("/").length;
const depthB = b.value.split("/").length;
return depthA - depthB;
});
Комбинация этих подходов позволяет управлять порядком отображения иерархий.
При использовании load (AJAX) вложенные группы часто
формируются на лету.
new TomSelect("#select", {
load: function(query, callback) {
fetch(`/api/options?q=${query}`)
.then(res => res.json())
.then(data => {
const { groups, options } = flattenTree(data);
this.addOption(options);
this.addOptionGroup(groups);
callback(options);
});
}
});
Особенность динамической модели:
При увеличении вложенности возникают следующие узкие места:
Оптимизационные подходы:
Вложенная структура в интерфейсе селекта влияет на поведение пользователя:
В ряде случаев вместо вложенных групп используется компромиссная модель:
{
value: "react",
text: "Frontend / Frameworks / React"
}
Такой подход полностью совместим с Tom Select без кастомных рендеров и снижает сложность поддержки.
Хотя Tom Select не поддерживает сворачивание групп, поведение можно эмулировать через CSS и обработчики событий.
onInitialize: function() {
this.dropdown_content.addEventListener("click", (e) => {
const header = e.target.closest(".optgroup-header");
if (!header) return;
const group = header.dataset.group;
document
.querySelectorAll(`[data-group="${group}"]`)
.forEach(el => el.classList.toggle("hidden"));
});
}
Такой подход переводит группировку в интерактивную модель поверх базовой структуры.
При всей гибкости реализации сохраняются фундаментальные ограничения:
Эти ограничения определяют необходимость предварительной нормализации данных и строгого контроля структуры до передачи в Tom Select.