Зависимые выпадающие списки в Choices.js представляют собой механизм каскадного обновления содержимого одного селекта на основе значения другого. Такая архитектура применяется при построении форм с иерархическими данными: страна → регион → город, категория → подкатегория → товар, тип пользователя → набор прав и т.д.
Зависимые списки строятся на принципе родитель–потомок, где изменение состояния родительского компонента приводит к полной или частичной переработке данных дочернего.
Ключевые элементы:
В контексте 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
);
}
Такая логика предотвращает ввод некорректных данных.
При сложных формах важно минимизировать вызовы обновления:
setChoiceschangelet 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.
Проблема возникает при быстром переключении значений:
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 даже при нестабильной сети.