Древовидные структуры в интерфейсе выбора используются тогда, когда
набор данных имеет иерархию: категории, подкатегории и элементы. В
контексте веб-форм это чаще всего выражается через селекторы с
группировкой и многоуровневой логикой выбора. Библиотека 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:
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>, которые
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>
`);
}
};
}
});
Это даёт полный контроль над визуальной структурой и позволяет внедрять, например, иконки раскрытия или индикаторы ветвей.
При работе с большими иерархиями важно учитывать производительность. Плоское представление может содержать тысячи элементов, что влияет на:
Оптимизация достигается через:
Пример ленивой загрузки:
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 всегда является имитацией, а не нативной функцией. Основные ограничения:
При проектировании интерфейса важно учитывать, что Choices.js оптимизирован под селекты, а не под полноценные tree-view компоненты.
В случаях, когда требуется полноценное дерево с раскрытием узлов, часто используется гибридный подход: Choices.js применяется только для поиска и мультивыбора, а дерево отображается отдельным компонентом, синхронизированным через JavaScript-состояние.
Такой подход позволяет сохранить удобство выбора и одновременно обеспечить сложную структуру данных без перегрузки UI.