Каскадные выпадающие списки

Каскадные выпадающие списки (dependent dropdowns) в Slim Select реализуются через управление состоянием нескольких инстансов селекта и динамическое обновление их опций на основе выбора в родительском списке. Основная идея заключается в том, что значение одного select определяет набор доступных значений в следующем, формируя цепочку зависимостей уровня «страна → регион → город», «категория → подкатегория → товар» и аналогичных структур.

Каскадные списки строятся вокруг иерархических данных. На практике это чаще всего JSON-структура:

const data = {
  kazakhstan: {
    karaganda: ["Balkhash", "Temirtau"],
    astana: ["Esil", "Almaty District"]
  },
  russia: {
    moscow: ["Central", "North"],
    spb: ["Nevsky", "Primorsky"]
  }
};

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

Инициализация Slim Select для нескольких уровней

Каждый уровень каскада — отдельный экземпляр Slim Select.

<select id="country"></select>
<select id="region"></select>
<select id="city"></select>
const countrySelect = new SlimSelect({
  select: '#country'
});

const regionSelect = new SlimSelect({
  select: '#region'
});

const citySelect = new SlimSelect({
  select: '#city'
});

Первоначально нижние уровни обычно инициализируются пустыми или с disabled состоянием.

Заполнение первого уровня

Первый select не зависит ни от чего и заполняется статически:

countrySelect.setData([
  { text: 'Kazakhstan', value: 'kazakhstan' },
  { text: 'Russia', value: 'russia' }
]);

После выбора страны становится возможным построение следующего уровня.

Обработка события изменения

Ключевой механизм каскада — обработка события изменения значения.

document.querySelector('#country').addEventListener('change', (e) => {
  const country = e.target.value;

  updateRegions(country);
  resetCities();
});

Функция updateRegions отвечает за формирование второго уровня.

Динамическое обновление второго уровня

Slim Select не требует пересоздания инстанса для обновления данных. Используется метод setData.

function updateRegions(country) {
  const regions = data[country];

  const options = Object.keys(regions).map(region => ({
    text: region,
    value: region
  }));

  regionSelect.setData(options);
}

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

regionSelect.set('');

Сброс зависимых уровней

Каскадная логика требует строгого сброса всех дочерних селектов при изменении родителя.

function resetCities() {
  citySelect.setData([]);
  citySelect.set('');
}

При изменении региона аналогично пересоздается список городов.

document.querySelector('#region').addEventListener('change', (e) => {
  const region = e.target.value;
  const country = document.querySelector('#country').value;

  updateCities(country, region);
});

Построение третьего уровня

function updateCities(country, region) {
  const cities = data[country][region];

  const options = cities.map(city => ({
    text: city,
    value: city
  }));

  citySelect.setData(options);
}

Управление состоянием disabled

Каскадные списки часто требуют блокировки зависимых уровней до выбора родителя.

regionSelect.disable();
citySelect.disable();

После выбора страны:

regionSelect.enable();

После выбора региона:

citySelect.enable();

Это предотвращает некорректные состояния интерфейса.

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

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

async function fetchRegions(country) {
  const res = await fetch(`/api/regions?country=${country}`);
  return await res.json();
}

Интеграция с Slim Select:

async function updateRegions(country) {
  regionSelect.setData([]);
  regionSelect.disable();

  const regions = await fetchRegions(country);

  regionSelect.setData(
    regions.map(r => ({ text: r.name, value: r.id }))
  );

  regionSelect.enable();
}

Кеширование результатов

Чтобы избежать повторных запросов, используется локальный кеш.

const cache = {};

async function getRegions(country) {
  if (cache[country]) return cache[country];

  const data = await fetchRegions(country);
  cache[country] = data;

  return data;
}

Это особенно важно при частых переключениях значений.

Сложные каскады с несколькими зависимостями

При расширении логики до 4–5 уровней важно централизовать управление состоянием.

function resetFrom(level) {
  if (level <= 2) {
    regionSelect.setData([]);
    regionSelect.set('');
    regionSelect.disable();
  }

  if (level <= 3) {
    citySelect.setData([]);
    citySelect.set('');
    citySelect.disable();
  }
}

Защита от race conditions

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

Решение — контроль актуальности запроса:

let currentRequestId = 0;

async function updateRegions(country) {
  const requestId = ++currentRequestId;

  const regions = await fetchRegions(country);

  if (requestId !== currentRequestId) return;

  regionSelect.setData(
    regions.map(r => ({ text: r.name, value: r.id }))
  );
}

Взаимосвязь событий между уровнями

Иногда требуется реакция не только на пользовательские события, но и на программные изменения.

countrySelect.onCha nge = () => {
  const value = countrySelect.getSelected();

  updateRegions(value);
  resetCities();
};

Работа с предварительно выбранными значениями

При редактировании формы необходимо восстановить каскадное состояние.

async function initForm(country, region, city) {
  countrySelect.set(country);

  await updateRegions(country);
  regionSelect.set(region);

  await updateCities(country, region);
  citySelect.set(city);
}

Ключевой момент — строгое последовательное ожидание загрузки данных.

Оптимизация большого количества зависимостей

При масштабных формах каскады становятся дорогими по производительности. Используются техники:

  • ленивой загрузки уровней
  • частичного обновления setData
  • минимизации перерисовок DOM
  • кеширования ответов API

Общая архитектурная модель

Каскадные списки с Slim Select фактически реализуют реактивную цепочку:

  • изменение состояния родителя
  • пересчет данных дочернего уровня
  • обновление UI через setData
  • сброс старого состояния
  • блокировка до загрузки новых данных

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