Вложенные структуры данных в контексте Tom Select представляют собой
способ организации элементов выбора в виде дерева или групп с уровнями
вложенности. На практике это выражается через optgroup,
дочерние массивы children, а также пользовательские схемы
данных, поступающие из API.
Основная задача работы с вложенностью — корректно отобразить сложные справочники: категории товаров, иерархии регионов, структуру организаций, древовидные классификаторы.
Наиболее простой вариант вложенности в Tom Select реализуется через
optgroup.
new TomSelect("#select", {
options: [
{ value: "js", text: "JavaScript", group: "frontend" },
{ value: "ts", text: "TypeScript", group: "frontend" },
{ value: "node", text: "Node.js", group: "backend" }
],
optgroups: [
{ value: "frontend", label: "Frontend" },
{ value: "backend", label: "Backend" }
],
optgroupField: "group",
labelField: "text",
valueField: "value"
});
Здесь вложенность реализуется на уровне одного поля
group, которое связывает элемент с группой. Это не дерево в
строгом смысле, а плоская структура с логической группировкой.
Для настоящей иерархии используется поле children. Оно
позволяет строить рекурсивные структуры.
const data = [
{
text: "Фронтенд",
children: [
{ value: "react", text: "React" },
{ value: "vue", text: "Vue" }
]
},
{
text: "Бэкенд",
children: [
{ value: "django", text: "Django" },
{ value: "express", text: "Express" }
]
}
];
new TomSelect("#select", {
options: data,
valueField: "value",
labelField: "text",
optgroupField: "optgroup"
});
Несмотря на наличие children, внутренняя модель Tom
Select не всегда рендерит дерево рекурсивно как файловый менеджер — чаще
всего происходит разворачивание до уровня групп.
Часто данные приходят из API в глубоко вложенном виде:
[
{
"name": "IT",
"nodes": [
{
"name": "Frontend",
"nodes": [
{ "id": 1, "name": "React" },
{ "id": 2, "name": "Angular" }
]
}
]
}
]
Для использования в Tom Select требуется преобразование:
function normalize(nodes, parent = null) {
const result = [];
nodes.forEach(node => {
if (node.nodes) {
result.push({
text: node.name,
children: normalize(node.nodes, node.name)
});
} else {
result.push({
value: node.id,
text: node.name,
parent
});
}
});
return result;
}
После нормализации структура становится совместимой с
children-моделью.
Вложенные структуры часто требуют динамической подгрузки.
new TomSelect("#select", {
load: function(query, callback) {
fetch(`/api/categories?search=${query}`)
.then(res => res.json())
.then(data => {
callback(data);
})
.catch(() => callback());
}
});
Если сервер возвращает дерево, его необходимо либо:
Вложенные структуры в Tom Select могут требовать имитации “раскрытия”.
Типовой подход — хранение состояния раскрытия:
const expanded = new Set();
function toggle(nodeId) {
if (expanded.has(nodeId)) {
expanded.delete(nodeId);
} else {
expanded.add(nodeId);
}
}
Далее данные фильтруются перед передачей в options.
Поиск в иерархии требует рекурсивного обхода:
function filterTree(nodes, query) {
const result = [];
nodes.forEach(node => {
if (node.children) {
const filtered = filterTree(node.children, query);
if (filtered.length) {
result.push({ ...node, children: filtered });
}
} else if (node.text.toLowerCase().includes(query.toLowerCase())) {
result.push(node);
}
});
return result;
}
Внутренний search механизм Tom Select по умолчанию не полностью учитывает глубокую вложенность, поэтому предварительная фильтрация часто необходима.
Для отображения уровней используется render:
new TomSelect("#select", {
options: data,
render: {
option: function(data) {
const depth = data.depth || 0;
return `
<div style="padding-left:${depth * 12}px">
${data.text}
</div>
`;
}
}
});
Глубина может вычисляться при нормализации:
function addDepth(nodes, depth = 0) {
return nodes.map(n => {
if (n.children) {
return {
...n,
depth,
children: addDepth(n.children, depth + 1)
};
}
return { ...n, depth };
});
}
В сложных структурах важно строго разделять:
valueField — уникальный идентификаторlabelField — отображаемое имяoptgroupField — связь с группойnew TomSelect("#select", {
valueField: "id",
labelField: "name",
optgroupField: "category"
});
При вложенности ошибка часто возникает, когда вложенные узлы не имеют
уникального value, что приводит к конфликтам выбора.
Смешанная модель используется при частично иерархических данных:
const data = [
{
text: "Languages",
children: [
{
text: "JavaScript",
children: [
{ value: "js-es5", text: "ES5" },
{ value: "js-es6", text: "ES6+" }
]
}
]
}
];
В Tom Select такая структура чаще всего упрощается до одного уровня группировки, но кастомные рендеры позволяют сохранить визуальную глубину.
При работе с вложенными структурами важно учитывать, что выбранные значения всегда плоские:
const selected = select.getValue();
Если необходимо восстановить контекст дерева:
function findPath(nodes, value, path = []) {
for (const node of nodes) {
if (node.value === value) return [...path, node];
if (node.children) {
const result = findPath(node.children, value, [...path, node]);
if (result) return result;
}
}
}
Иногда вложенность полностью устраняется ради производительности:
[
{ value: 1, text: "React", group: "Frontend" },
{ value: 2, text: "Vue", group: "Frontend" },
{ value: 3, text: "Node", group: "Backend" }
]
Tom Select эффективно работает с таким форматом быстрее, чем с глубокими деревьями, особенно при большом количестве элементов.
При увеличении глубины вложенности возникают типичные проблемы:
Типовые оптимизации:
filterTreeconst index = new Map();
function buildIndex(nodes) {
nodes.forEach(n => {
index.set(n.value, n);
if (n.children) buildIndex(n.children);
});
}
При включённом create: true необходимо учитывать уровень
вставки:
new TomSelect("#select", {
create: true,
onOptionAdd: function(value, data) {
return {
value,
text: value,
children: []
};
}
});
Вложенные структуры требуют явного указания, куда добавляется новый элемент — в корень или в текущий узел.
При динамическом обновлении вложенных структур:
select.clearOptions();
select.addOptions(newData);
важно сохранять выбранные значения:
const selected = select.getValue();
select.clearOptions();
select.addOptions(newData);
select.setValue(selected);
Это особенно критично при глубоких иерархиях, где пересборка дерева может разрушить ссылки на узлы.