Древовидный селектор

Древовидные структуры в интерфейсе выбора используются тогда, когда набор данных имеет иерархию: категории, подкатегории и элементы. В контексте веб-форм это чаще всего выражается через селекторы с группировкой и многоуровневой логикой выбора. Библиотека Choices.js изначально ориентирована на улучшение стандартных элементов <select> и <input>, предоставляя расширенный UI, поиск, мультивыбор и управление тегами, однако полноценная древовидная структура требует дополнительного проектирования поверх базовых возможностей.

Основная проблема реализации дерева в Choices.js заключается в отсутствии встроенной поддержки вложенной иерархии глубже, чем стандартный уровень <optgroup>. Поэтому реализация строится на комбинации группировки, кастомных данных и расширения отображения через API рендера.

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

const treeData = [
  {
    label: "Электроника",
    value: "electronics",
    children: [
      {
        label: "Телефоны",
        value: "phones",
        children: [
          { label: "Смартфоны", value: "smartphones" },
          { label: "Кнопочные телефоны", value: "feature-phones" }
        ]
      },
      {
        label: "Ноутбуки",
        value: "laptops"
      }
    ]
  },
  {
    label: "Бытовая техника",
    value: "appliances",
    children: [
      {
        label: "Кухня",
        value: "kitchen"
      }
    ]
  }
];

Choices.js не умеет напрямую обрабатывать поле children, поэтому требуется преобразование в поддерживаемый формат.

Преобразование дерева в плоский список

Библиотека работает с объектами вида { value, label, selected, disabled }. Для имитации дерева вводится дополнительное поле customProperties, в котором можно хранить уровень вложенности.

function flattenTree(nodes, level = 0, parent = null) {
  return nodes.flatMap(node => {
    const item = {
      value: node.value,
      label: node.label,
      selected: false,
      disabled: false,
      customProperties: {
        level,
        parent
      }
    };

    const children = node.children
      ? flattenTree(node.children, level + 1, node.value)
      : [];

    return [item, ...children];
  });
}

const flatData = flattenTree(treeData);

Такой подход позволяет сохранить иерархию без потери совместимости с Choices.js.

Инициализация Choices.js с иерархическими данными

После подготовки данных они передаются в экземпляр Choices:

import Choices from "choices.js";

const element = document.querySelector("#tree-select");

const choices = new Choices(element, {
  choices: flatData,
  searchEnabled: true,
  itemSelectText: "",
  shouldSort: false
});

На этом этапе интерфейс будет отображать элементы, но без визуального различия уровней. Следующий шаг — стилизация.

Визуализация уровней вложенности

Иерархия достигается через CSS, используя data-custom-properties-level. Choices.js автоматически сериализует customProperties в data-атрибуты.

Пример стилизации:

.choices__item--choice[data-custom-properties-level="0"] {
  font-weight: 600;
}

.choices__item--choice[data-custom-properties-level="1"] {
  padding-left: 20px;
  font-weight: 500;
}

.choices__item--choice[data-custom-properties-level="2"] {
  padding-left: 40px;
  font-weight: 400;
  color: #444;
}

Таким образом создаётся визуальный эффект дерева без изменения внутренней логики компонента.

Группировка через optgroup как альтернативный подход

Если глубина структуры ограничена одним уровнем вложенности, можно использовать стандартные группы <optgroup>, которые Choices.js поддерживает нативно.

HTML-структура:

<select id="group-select" multiple>
  <optgroup label="Фрукты">
    <option value="apple">Яблоко</option>
    <option value="banana">Банан</option>
  </optgroup>
  <optgroup label="Овощи">
    <option value="carrot">Морковь</option>
    <option value="potato">Картофель</option>
  </optgroup>
</select>

Инициализация:

const groupSelect = document.querySelector("#group-select");

const choices = new Choices(groupSelect, {
  removeItemButton: true,
  searchEnabled: true
});

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

Управление поведением выбора в иерархии

При работе с древовидными структурами часто требуется логика каскадного выбора: выбор родителя должен автоматически влиять на потомков.

Choices.js не содержит встроенных механизмов каскадности, поэтому добавляется внешняя обработка:

function getChildren(parentValue, flatData) {
  return flatData.filter(item =>
    item.customProperties.parent === parentValue
  );
}

function selectChildren(parentValue) {
  const children = getChildren(parentValue, flatData);

  children.forEach(child => {
    choices.setChoiceByValue(child.value);
  });
}

Подобная логика может быть расширена до рекурсивного обхода всех уровней.

Обработка снятия выбора и синхронизация состояния

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

function deselectChildren(parentValue) {
  const children = getChildren(parentValue, flatData);

  children.forEach(child => {
    choices.removeActiveItemsByValue(child.value);

    // рекурсивное снятие
    deselectChildren(child.value);
  });
}

Для интеграции используется событие change:

element.addEventListener("change", (event) => {
  const value = event.detail.value;

  if (!event.detail.choice.selected) {
    deselectChildren(value);
  }
});

Кастомизация рендера элементов

Choices.js позволяет переопределять шаблоны через конфигурацию callbackOnCreateTemplates. Это используется для более явного отображения дерева.

const choices = new Choices(element, {
  callbackOnCreateTemplates: function (template) {
    return {
      item: (classNames, data) => {
        const level = data.customProperties?.level || 0;

        return template(`
          <div class="${classNames.item} ${classNames.itemSelectable}"
               data-value="${data.value}"
               data-level="${level}">
            ${data.label}
          </div>
        `);
      }
    };
  }
});

Это даёт полный контроль над визуальной структурой и позволяет внедрять, например, иконки раскрытия или индикаторы ветвей.

Оптимизация при больших деревьях

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

  • скорость рендера выпадающего списка
  • работу поиска
  • обновление состояния

Оптимизация достигается через:

  • ленивую загрузку ветвей
  • фильтрацию данных до передачи в Choices
  • виртуализацию отображения (внешними средствами)

Пример ленивой загрузки:

async function loadBranch(parentValue) {
  const response = await fetch(`/api/tree?parent=${parentValue}`);
  const data = await response.json();

  const items = flattenTree(data);
  choices.setChoices(items, "value", "label", true);
}

Поиск в древовидной структуре

Поиск в Choices.js работает по плоской модели, поэтому иерархия не учитывается автоматически. При необходимости поиска с учётом дерева используется предварительная фильтрация:

function searchTree(query, data) {
  return data.filter(item =>
    item.label.toLowerCase().includes(query.toLowerCase())
  );
}

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

label: `${parentLabel} / ${node.label}`

Ограничения подхода

Реализация дерева в Choices.js всегда является имитацией, а не нативной функцией. Основные ограничения:

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

При проектировании интерфейса важно учитывать, что Choices.js оптимизирован под селекты, а не под полноценные tree-view компоненты.

Расширение поведения через внешние компоненты

В случаях, когда требуется полноценное дерево с раскрытием узлов, часто используется гибридный подход: Choices.js применяется только для поиска и мультивыбора, а дерево отображается отдельным компонентом, синхронизированным через JavaScript-состояние.

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