Вложенные группы

Вложенные группы в Tom Sel ect опираются на базовый механизм optgroup, который в стандартной реализации ограничен одним уровнем иерархии. При этом сама архитектура библиотеки позволяет реализовать многоуровневую группировку через преобразование данных, кастомные шаблоны рендера и управление структурой источника опций.

В основе групп лежит поле optgroup, которое связывает каждую опцию с конкретной группой. Структура данных обычно выглядит следующим образом:

  • список options
  • список optgroups
  • поле связи optgroupField
new 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.

Ограничение стандартного optgroup

Стандартная модель группировки в Tom Select не поддерживает вложенные группы напрямую. Следующие ограничения фиксированы:

  • optgroup не может содержать другие optgroup
  • структура групп всегда плоская
  • вложенность не интерпретируется движком
  • рендер групп выполняется на одном уровне DOM

Это означает, что любая иерархия должна быть преобразована до передачи в компонент.

Моделирование иерархии через плоские группы

Основной подход к реализации вложенных групп — преобразование дерева в линейный список с кодированием пути.

Структура данных с путями

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 → уровень 0
  • Frontend/Frameworks → уровень 1
  • Frontend/Frameworks/Advanced → уровень 2

CSS-оформление:

.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 — плоский список optgroup
  • options — элементы с привязкой к группам

Поведение поиска при вложенных группах

Фильтрация в Tom Select не учитывает иерархию групп как отдельную структуру. Поиск работает по:

  • text
  • value
  • дополнительным полям через searchField

При вложенной структуре важно учитывать:

  • совпадение может происходить в дочерних элементах
  • родительская группа может отображаться даже без совпадений в заголовке
  • скрытие пустых групп требует дополнительной логики

Удаление пустых групп

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);
      });
  }
});

Особенность динамической модели:

  • группы могут появляться постепенно
  • структура дерева не обязана быть полной
  • необходимо контролировать дубликаты групп

Производительность при глубокой иерархии

При увеличении вложенности возникают следующие узкие места:

  • рост количества optgroup DOM-узлов
  • перерасчёт фильтрации при каждом вводе
  • увеличение времени рендера списка

Оптимизационные подходы:

  • кеширование результатов flattenTree
  • предварительная нормализация данных на сервере
  • ограничение глубины отображения
  • виртуализация списка через кастомные плагины

UX-особенности вложенных групп

Вложенная структура в интерфейсе селекта влияет на поведение пользователя:

  • глубокая иерархия снижает скорость выбора
  • визуальная вложенность должна быть ограничена 2–3 уровнями
  • важна ясная типографика для различения уровней
  • рекомендуется сохранять единый стиль отступов

Альтернативная модель: плоский интерфейс с контекстом

В ряде случаев вместо вложенных групп используется компромиссная модель:

  • один уровень optgroup
  • контекст вложенности добавляется в текст
{
  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"));
  });
}

Такой подход переводит группировку в интерактивную модель поверх базовой структуры.

Ограничения архитектуры вложенных групп

При всей гибкости реализации сохраняются фундаментальные ограничения:

  • отсутствует нативная рекурсивная модель групп
  • DOM всегда плоский
  • логика вложенности полностью лежит на разработчике
  • синхронизация состояния требует ручного управления

Эти ограничения определяют необходимость предварительной нормализации данных и строгого контроля структуры до передачи в Tom Select.