Библиотека Choices.js предоставляет механизм группировки элементов
списка через использование структуры, аналогичной HTML-элементу
<optgroup>. Базовый уровень группировки представляет
собой плоский набор категорий, внутри которых находятся опции. Однако в
ряде сценариев требуется более сложная организация — вложенные группы,
где категории формируют иерархическую структуру.
В стандартной модели данных Choices.js каждый элемент может
принадлежать группе, описанной через поле choices внутри
объекта группы:
const data = [
{
label: 'Фрукты',
id: 'fruits',
choices: [
{ value: 'apple', label: 'Яблоко' },
{ value: 'banana', label: 'Банан' }
]
},
{
label: 'Овощи',
id: 'vegetables',
choices: [
{ value: 'carrot', label: 'Морковь' },
{ value: 'potato', label: 'Картофель' }
]
}
];
Эта структура формирует один уровень группировки. Для реализации вложенности требуется расширение модели данных за счёт рекурсивного описания групп.
Вложенные группы строятся на основе принципа рекурсии: группа может содержать как элементы выбора, так и другие группы. Хотя Choices.js не предоставляет полноценной встроенной поддержки многоуровневых optgroup, структура данных может быть подготовлена заранее, а рендеринг адаптирован через кастомные шаблоны.
Пример рекурсивной структуры:
const nestedData = [
{
label: 'Еда',
choices: [
{
label: 'Фрукты',
choices: [
{ value: 'apple', label: 'Яблоко' },
{ value: 'pear', label: 'Груша' }
]
},
{
label: 'Овощи',
choices: [
{ value: 'carrot', label: 'Морковь' },
{ value: 'onion', label: 'Лук' }
]
}
]
}
];
Такая структура формально выходит за пределы стандартного API, но может быть преобразована перед передачей в экземпляр Choices.
Choices.js ожидает один уровень группировки, поэтому вложенные группы требуют предварительной нормализации. Основной подход заключается в рекурсивном обходе дерева и генерации уникальных меток групп.
Алгоритм преобразования:
Пример реализации:
function flattenGroups(nodes, parentLabel = '') {
let result = [];
nodes.forEach(node => {
const currentLabel = parentLabel
? `${parentLabel} / ${node.label}`
: node.label;
if (node.choices) {
const children = flattenGroups(node.choices, currentLabel);
result = result.concat(children);
} else {
result.push({
value: node.value,
label: node.label,
customProperties: {
groupPath: parentLabel
}
});
}
});
return result;
}
В результате получается плоский массив, пригодный для передачи в
Choices.js, при этом информация об иерархии сохраняется в
customProperties.
После преобразования данных структура передаётся в конструктор:
import Choices from 'choices.js';
const flattened = flattenGroups(nestedData);
const element = document.querySelector('#select');
const choices = new Choices(element, {
choices: flattened,
searchEnabled: true,
shouldSort: false
});
Хотя визуально группы исчезают, их логическая структура сохраняется через дополнительные поля, что позволяет реализовать кастомный рендеринг.
Для визуализации иерархии используется модификация шаблонов
choiceTemplate и itemSelectText. Choices.js
позволяет переопределять отображение элементов списка.
Пример добавления отступов на основе уровня вложенности:
const choices = new Choices(element, {
choices: flattened,
callbackOnCreateTemplates: function (template) {
return {
choice: (classNames, data) => {
const depth = (data.customProperties?.groupPath || '').split('/').length - 1;
const indent = depth * 12;
return template(`
<div class="${classNames.item} ${classNames.itemChoice}"
data-choice
data-id="${data.id}"
data-value="${data.value}">
<span style="padding-left:${indent}px">
${data.label}
</span>
</div>
`);
}
};
}
});
Такой подход создаёт визуальную иллюзию вложенности, несмотря на отсутствие реальных nested optgroup.
При работе с вложенными структурами важно сохранять информацию о принадлежности элемента к конкретной ветке дерева. Для этого используются:
customPropertiesПример хранения метаданных:
{
value: 'apple',
label: 'Яблоко',
customProperties: {
path: ['Еда', 'Фрукты']
}
}
Такой формат позволяет выполнять фильтрацию и группировку уже на уровне бизнес-логики, независимо от UI-ограничений библиотеки.
Стандартный поиск Choices.js работает по label и
value, игнорируя структуру групп. Для вложенных данных
часто требуется расширение логики поиска с учётом пути группы.
Переопределение фильтрации:
const choices = new Choices(element, {
choices: flattened,
searchFloor: 1,
fuseOptions: {
includeScore: true,
threshold: 0.3,
keys: ['label', 'customProperties.groupPath']
}
});
Интеграция с Fuse.js-логикой (встроенной в Choices.js или кастомной) позволяет учитывать иерархический контекст при поиске.
При работе с большими деревьями данных вложенные группы часто подгружаются лениво. В этом случае структура формируется на сервере частями.
Пример стратегии:
async function loadGroup(path) {
const response = await fetch(`/api/groups?path=${path}`);
return await response.json();
}
Далее новые элементы добавляются в Choices:
choices.setChoices(newItems, 'value', 'label', true);
Важно поддерживать согласованность структуры, чтобы элементы не теряли контекст вложенности при добавлении.
Модель вложенных групп в Choices.js имеет ряд ограничений:
По этой причине вложенные группы реализуются преимущественно как логическая модель данных, а не как встроенный UI-элемент библиотеки.
При масштабных структурах (каталоги товаров, классификаторы, справочники) используется комбинированный подход:
Такой подход снижает нагрузку на DOM и улучшает производительность при большом количестве элементов.