Каскадные выпадающие списки представляют собой механизм, при котором содержимое одного селектора зависит от выбранного значения в другом. Такая структура используется для построения иерархий: страна → регион → город, категория → подкатегория → товар, год → месяц → день и аналогичных зависимостей.
В Choices.js каскадная логика не встроена как отдельная функция, но реализуется через управление состоянием экземпляров, динамическое обновление данных и использование событий выбора.
Каскад обычно состоит из нескольких уровней:
Каждый уровень хранит свой экземпляр Choices, а связь
между ними обеспечивается через обработчики событий.
Создание базовой структуры начинается с инициализации независимых экземпляров:
const countrySelect = new Choices('#country', {
searchEnabled: true,
shouldSort: false
});
const citySelect = new Choices('#city', {
searchEnabled: true,
shouldSort: false
});
На этом этапе селекты не связаны между собой, но уже готовы к динамическому обновлению.
Основной механизм каскада строится на событии изменения значения родительского селектора.
Choices.js предоставляет событие change через
DOM-элемент:
const countryElement = document.querySelector('#country');
countryElement.addEventListener('change', (event) => {
const selectedCountry = event.target.value;
updateCities(selectedCountry);
});
Функция updateCities отвечает за перезаполнение
зависимого списка.
Для корректного обновления необходимо:
Choices.js предоставляет методы clearChoices() и
setChoices().
function updateCities(country) {
const cities = getCitiesByCountry(country);
citySelect.clearChoices();
citySelect.setChoices(
cities.map(city => ({
value: city.id,
label: city.name,
selected: false,
disabled: false
})),
'value',
'label',
true
);
citySelect.removeActiveItems();
}
Чаще всего данные хранятся в виде вложенной структуры:
const data = {
kazakhstan: [
{ id: 'karaganda', name: 'Караганда' },
{ id: 'almaty', name: 'Алматы' }
],
russia: [
{ id: 'moscow', name: 'Москва' },
{ id: 'spb', name: 'Санкт-Петербург' }
]
};
function getCitiesByCountry(country) {
return data[country] || [];
}
При изменении верхнего уровня необходимо сбрасывать все нижние уровни, иначе возникает неконсистентное состояние.
function resetCitySelect() {
citySelect.clearChoices();
citySelect.removeActiveItems();
}
Если каскад имеет больше уровней, сброс выполняется рекурсивно вниз по цепочке.
Для цепочек из трёх и более селекторов логика расширяется:
const countrySelect = new Choices('#country');
const regionSelect = new Choices('#region');
const citySelect = new Choices('#city');
document.querySelector('#country').addEventListener('change', (e) => {
const country = e.target.value;
updateRegions(country);
regionSelect.removeActiveItems();
citySelect.clearChoices();
citySelect.removeActiveItems();
});
function updateRegions(country) {
const regions = getRegions(country);
regionSelect.clearChoices();
regionSelect.setChoices(
regions.map(r => ({
value: r.id,
label: r.name
})),
'value',
'label',
true
);
}
В реальных приложениях каскады часто зависят от API.
async function updateCities(country) {
citySelect.clearChoices();
const response = await fetch(`/api/cities?country=${country}`);
const cities = await response.json();
citySelect.setChoices(
cities,
'id',
'name',
true
);
}
Асинхронная модель позволяет загружать данные по мере необходимости, уменьшая начальный вес страницы.
Во время загрузки данных зависимые селекторы часто временно отключаются:
function setLoading(selectInstance, state) {
const element = selectInstance.passedElement.element;
element.disabled = state;
}
Использование:
setLoading(citySelect, true);
fetch('/api/cities')
.then(r => r.json())
.then(data => {
citySelect.setChoices(data, 'id', 'name', true);
})
.finally(() => {
setLoading(citySelect, false);
});
При смене значения родительского селектора важно полностью сбрасывать состояние зависимых элементов:
function fullReset(selectInstance) {
selectInstance.clearChoices();
selectInstance.removeActiveItems();
selectInstance.setChoiceByValue('');
}
Помимо DOM-событий, Choices.js предоставляет внутренние события через API:
countrySelect.passedElement.element.addEventListener(
'addItem',
function(event) {
console.log('Выбран элемент:', event.detail.value);
}
);
Это позволяет централизованно реагировать на изменения состояния.
В сложных интерфейсах несколько каскадов могут быть связаны между собой. Например:
Для предотвращения конфликтов используется единый контроллер состояния:
const state = {
country: null,
city: null
};
countrySelect.passedElement.element.addEventListener('change', (e) => {
state.country = e.target.value;
state.city = null;
updateCities(state.country);
});
Choices.js позволяет задавать начальные значения, что важно для редактирования форм:
citySelect.setChoices([
{ value: 'karaganda', label: 'Караганда', selected: true }
], 'value', 'label', true);
При каскадной логике важно предварительно загрузить зависимые данные до установки selected состояния.
Если у родительского селектора нет выбранного значения, зависимые списки должны быть очищены:
if (!country) {
citySelect.clearChoices();
citySelect.removeActiveItems();
return;
}
Это предотвращает отображение некорректных данных.
При интенсивной смене значений (например, быстрый выбор в родительском списке) полезно применять debounce:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
countrySelect.passedElement.element.addEventListener(
'change',
debounce((e) => updateCities(e.target.value), 300)
);
Чтобы избежать повторных запросов:
const cache = new Map();
async function getCities(country) {
if (cache.has(country)) {
return cache.get(country);
}
const res = await fetch(`/api/cities?country=${country}`);
const data = await res.json();
cache.set(country, data);
return data;
}
Основные проблемы возникают при неправильном управлении состоянием:
clearChoices()При быстром переключении значений возможна гонка запросов:
let requestId = 0;
async function updateCities(country) {
const currentId = ++requestId;
const res = await fetch(`/api/cities?country=${country}`);
const data = await res.json();
if (currentId !== requestId) return;
citySelect.setChoices(data, 'id', 'name', true);
}
Это гарантирует актуальность данных.
Каскадная структура может изменяться во время выполнения:
Choices.js позволяет управлять этим через полную пересборку
setChoices и пересоздание связей между экземплярами.