Зависимые селекты представляют собой цепочку связанных выпадающих списков, где выбор значения в одном селекте определяет доступные опции в другом. Такой подход используется при построении интерфейсов с иерархическими или контекстно-зависимыми данными: страны → города, категории → подкатегории, бренд → модель, год → модификация.
Библиотека Slim Select обеспечивает удобный слой абстракции над
стандартными <select> и позволяет динамически
управлять набором опций без ручного управления DOM-элементами.
Классическая схема зависимости строится по принципу направленного графа:
Основные свойства такой структуры:
Для работы с зависимыми селектами каждый элемент инициализируется отдельно.
<select id="country">
<option value="">Выбор страны</option>
</select>
<select id="city">
<option value="">Выбор города</option>
</select>
const countrySelect = new SlimSelect({
select: '#country'
});
const citySelect = new SlimSelect({
select: '#city'
});
На этом этапе оба селекта независимы, но уже готовы к динамическому управлению данными.
Логика зависимости строится через обработчик события изменения значения родителя.
const data = {
kazakhstan: [
{ text: 'Караганда', value: 'karaganda' },
{ text: 'Алматы', value: 'almaty' }
],
turkey: [
{ text: 'Стамбул', value: 'istanbul' },
{ text: 'Анкара', value: 'ankara' }
]
};
const citySelect = new SlimSelect({
select: '#city',
data: []
});
document.querySelector('#country').addEventListener('change', (e) => {
const value = e.target.value;
const cities = data[value] || [];
citySelect.setData(cities);
});
Ключевой механизм здесь — метод setData, полностью
заменяющий набор опций в дочернем селекте.
При смене родительского значения необходимо очищать текущее состояние зависимых селектов.
document.querySelector('#country').addEventListener('change', (e) => {
const value = e.target.value;
citySelect.set('');
citySelect.setData(data[value] || []);
});
Сброс предотвращает сохранение некорректного состояния, когда ранее выбранный город не принадлежит новой стране.
Распространённая практика — отключение зависимого селекта до появления валидного контекста.
const citySelectInstance = new SlimSelect({
select: '#city',
disabled: true,
data: []
});
document.querySelector('#country').addEventListener('change', (e) => {
const value = e.target.value;
if (!value) {
citySelectInstance.set('');
citySelectInstance.setData([]);
citySelectInstance.disable();
return;
}
citySelectInstance.enable();
citySelectInstance.setData(data[value]);
});
Такой подход снижает вероятность неконсистентных пользовательских состояний.
В реальных системах данные редко хранятся в статических объектах. Чаще используется серверный API.
const citySelectInstance = new SlimSelect({
select: '#city',
data: [],
placeholder: 'Загрузка недоступна'
});
document.querySelector('#country').addEventListener('change', async (e) => {
const country = e.target.value;
citySelectInstance.set('');
citySelectInstance.setData([]);
if (!country) return;
const response = await fetch(`/api/cities?country=${country}`);
const cities = await response.json();
const formatted = cities.map(item => ({
text: item.name,
value: item.id
}));
citySelectInstance.setData(formatted);
});
В такой модели Slim Select выступает как рендер-слой, а вся логика агрегации данных переносится в API.
Цепочки могут включать более двух уровней:
Принцип построения остаётся одинаковым: каждый уровень зависит только от предыдущего.
country → region → city → district
Реализация:
countrySelect.addEventListener('change', async (e) => {
const regions = await loadRegions(e.target.value);
regionSelect.set('');
citySelect.set('');
districtSelect.set('');
regionSelect.setData(regions);
});
regionSelect.addEventListener('change', async (e) => {
const cities = await loadCities(e.target.value);
citySelect.set('');
districtSelect.set('');
citySelect.setData(cities);
});
Каждое изменение вверх по цепочке приводит к каскадному сбросу вниз.
В некоторых случаях обновление структуры данных настолько значительное, что проще пересоздать селект.
citySelect.destroy();
citySelect = new SlimSelect({
select: '#city',
data: newData
});
Такой подход применяется при смене режима отображения или при загрузке полностью новой модели данных.
При частых переключениях родительских значений оптимизация через кэш снижает количество запросов:
const cache = {};
async function getCities(country) {
if (cache[country]) return cache[country];
const res = await fetch(`/api/cities?country=${country}`);
const data = await res.json();
cache[country] = data;
return data;
}
Использование кэша особенно эффективно в интерфейсах с повторяющимися переключениями фильтров.
При любой смене родителя важно синхронно очищать все уровни ниже.
function resetChain() {
regionSelect.set('');
citySelect.set('');
districtSelect.set('');
regionSelect.setData([]);
citySelect.setData([]);
districtSelect.setData([]);
}
Такая функция предотвращает накопление устаревших значений в интерфейсе.
Частая проблема — частичное заполнение цепочки.
Защитная логика:
if (!country) {
regionSelect.disable();
citySelect.disable();
}
Или более строгая модель:
const isValid = Boolean(country && region);
if (!isValid) {
citySelect.setData([]);
}
При работе с тысячами элементов зависимые селекты требуют оптимизации:
setData вызововОсобенно критично избегать частых полных пересборок списка при каждом изменении фильтра.
parentSelect.addEventListener('change', async (e) => {
const value = e.target.value;
childSelect.set('');
childSelect.setData([]);
if (!value) return;
childSelect.disable();
const items = await load(value);
childSelect.setData(items);
childSelect.enable();
});
Такая структура остаётся базовым паттерном для любых каскадных интерфейсов, построенных на Slim Select.