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

Структура групп и иерархия данных

Библиотека 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 ожидает один уровень группировки, поэтому вложенные группы требуют предварительной нормализации. Основной подход заключается в рекурсивном обходе дерева и генерации уникальных меток групп.

Алгоритм преобразования:

  1. Обход дерева сверху вниз
  2. Формирование полного пути группы
  3. Перенос элементов в плоский список с составным label

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

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.


Инициализация Choices.js с группированными данными

После преобразования данных структура передаётся в конструктор:

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 имеет ряд ограничений:

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

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


Практическая организация больших деревьев

При масштабных структурах (каталоги товаров, классификаторы, справочники) используется комбинированный подход:

  • дерево хранится отдельно от UI
  • Choices.js получает только актуальный подмассив
  • иерархия передаётся через метаданные
  • визуализация упрощается до уровней отступов

Такой подход снижает нагрузку на DOM и улучшает производительность при большом количестве элементов.