Зависимые выпадающие списки

Зависимые выпадающие списки в Choices.js представляют собой механизм каскадного обновления содержимого одного селекта на основе значения другого. Такая архитектура применяется при построении форм с иерархическими данными: страна → регион → город, категория → подкатегория → товар, тип пользователя → набор прав и т.д.


Зависимые списки строятся на принципе родитель–потомок, где изменение состояния родительского компонента приводит к полной или частичной переработке данных дочернего.

Ключевые элементы:

  • Источник данных — статический объект или API
  • Родительский select — управляет контекстом
  • Дочерний select — обновляется динамически
  • Слой синхронизации — логика связывания

В контексте Choices.js каждый select обычно представлен отдельным экземпляром класса Choices.


Инициализация нескольких экземпляров

Для построения каскада создаются независимые инстансы:

const countrySelect = new Choices('#country', {
  searchEnabled: true,
  shouldSort: false
});

const citySelect = new Choices('#city', {
  searchEnabled: true,
  shouldSort: false
});

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


Структура данных для каскада

Чаще всего используется вложенная структура:

const data = {
  kazakhstan: [
    { value: 'karaganda', label: 'Караганда' },
    { value: 'astana', label: 'Астана' }
  ],
  russia: [
    { value: 'moscow', label: 'Москва' },
    { value: 'spb', label: 'Санкт-Петербург' }
  ]
};

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


Обработка изменения родительского списка

Основной механизм синхронизации строится на событии change.

countrySelect.passedElement.element.addEventListener('change', (event) => {
  const selectedCountry = event.detail.value || event.target.value;

  updateCityOptions(selectedCountry);
});

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


Обновление дочернего списка

Методика обновления обычно включает полную очистку и повторное заполнение:

function updateCityOptions(country) {
  citySelect.clearStore();

  const cities = data[country] || [];

  citySelect.setChoices(
    cities,
    'value',
    'label',
    true
  );
}

Метод clearStore() удаляет текущие опции, предотвращая смешивание старых и новых данных.

Метод setChoices() из Choices.js выполняет загрузку нового набора элементов.


Предотвращение неконсистентного состояния

При каскадной зависимости важно учитывать промежуточные состояния:

  • родитель изменился, но данные ещё не загружены
  • дочерний список содержит устаревшие значения
  • пользователь успел выбрать значение до обновления

Решение:

function resetCitySelect() {
  citySelect.disable();
  citySelect.clearStore();
}

После обновления:

citySelect.enable();

Асинхронная загрузка данных

В реальных приложениях данные часто приходят с сервера:

countrySelect.passedElement.element.addEventListener('change', async (event) => {
  const country = event.detail.value;

  citySelect.disable();
  citySelect.clearStore();

  const response = await fetch(`/api/cities?country=${country}`);
  const cities = await response.json();

  citySelect.setChoices(cities, 'value', 'label', true);
  citySelect.enable();
});

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


Каскад из трёх и более уровней

Расширение логики:

  • страна → регион → город
  • категория → подкатегория → товар → вариация

Пример цепочки:

countrySelect.passedElement.element.addEventListener('change', (e) => {
  updateRegions(e.detail.value);
  resetCitySelect();
});

regionSelect.passedElement.element.addEventListener('change', (e) => {
  updateCities(e.detail.value);
});

Каждый уровень полностью управляется вручную, поскольку Choices.js не предоставляет встроенного dependency graph.


Очистка и переинициализация

В некоторых случаях требуется полное пересоздание экземпляра:

citySelect.destroy();

citySelect = new Choices('#city', {
  searchEnabled: true
});

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


Фильтрация опций без перезагрузки

Если данные уже загружены, можно применять фильтрацию на клиенте:

function filterCities(country) {
  const filtered = allCities.filter(item => item.country === country);

  citySelect.clearStore();
  citySelect.setChoices(filtered, 'value', 'label', true);
}

Этот метод снижает нагрузку на сеть и ускоряет отклик интерфейса в Choices.js.


Сохранение выбранных значений

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

function updateCityOptions(country, previousValue) {
  const cities = data[country] || [];

  citySelect.clearStore();
  citySelect.setChoices(cities, 'value', 'label', true);

  const exists = cities.some(c => c.value === previousValue);

  if (exists) {
    citySelect.setChoiceByValue(previousValue);
  }
}

Метод setChoiceByValue() позволяет восстановить состояние после перерисовки.


Обработка пустых состояний

Когда родитель не выбран:

function handleEmptyCountry() {
  citySelect.clearStore();
  citySelect.setChoices(
    [{ value: '', label: 'Выберите страну сначала', disabled: true }],
    'value',
    'label',
    true
  );
}

Такая логика предотвращает ввод некорректных данных.


Оптимизация количества перерисовок

При сложных формах важно минимизировать вызовы обновления:

  • группировать изменения
  • избегать повторного setChoices
  • использовать debounce на change
let timeout;

countrySelect.passedElement.element.addEventListener('change', (e) => {
  clearTimeout(timeout);

  timeout = setTimeout(() => {
    updateCityOptions(e.detail.value);
  }, 150);
});

В Choices.js это особенно важно при больших наборах данных.


Синхронизация нескольких зависимых веток

Сложные формы могут иметь параллельные зависимости:

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

Пример:

function onCountryChange(country) {
  updateCities(country);
  updateCurrency(country);
  updatePhoneCode(country);
}

Такой подход формирует централизованный слой управления состоянием поверх Choices.js.


Обработка race conditions при AJAX

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

let requestId = 0;

countrySelect.passedElement.element.addEventListener('change', async (e) => {
  const currentId = ++requestId;

  const res = await fetch(`/api/cities?country=${e.detail.value}`);
  const data = await res.json();

  if (currentId !== requestId) return;

  citySelect.setChoices(data, 'value', 'label', true);
});

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


Деактивация дочерних списков при ошибках

При сбое API:

catch (error) {
  citySelect.disable();
  citySelect.clearStore();

  citySelect.setChoices(
    [{ value: '', label: 'Ошибка загрузки', disabled: true }],
    'value',
    'label',
    true
  );
}

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