Вложенные структуры данных

Вложенные структуры данных в контексте 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

Для настоящей иерархии используется поле 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-моделью.


Рекурсивная загрузка данных (lazy loading)

Вложенные структуры часто требуют динамической подгрузки.

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 и вложенных объектов

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

  • valueField — уникальный идентификатор
  • labelField — отображаемое имя
  • optgroupField — связь с группой
new TomSelect("#select", {
  valueField: "id",
  labelField: "name",
  optgroupField: "category"
});

При вложенности ошибка часто возникает, когда вложенные узлы не имеют уникального value, что приводит к конфликтам выбора.


Комбинирование optgroup и children

Смешанная модель используется при частично иерархических данных:

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 эффективно работает с таким форматом быстрее, чем с глубокими деревьями, особенно при большом количестве элементов.


Оптимизация работы с глубокими структурами

При увеличении глубины вложенности возникают типичные проблемы:

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

Типовые оптимизации:

  • кэширование результатов filterTree
  • ограничение глубины отображения
  • ленивое раскрытие узлов
  • предварительное преобразование в индексированный словарь
const index = new Map();

function buildIndex(nodes) {
  nodes.forEach(n => {
    index.set(n.value, n);
    if (n.children) buildIndex(n.children);
  });
}

Взаимодействие вложенности с create-поведением

При включённом 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);

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