Зависимые селекты

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

Библиотека Slim Select обеспечивает удобный слой абстракции над стандартными <select> и позволяет динамически управлять набором опций без ручного управления DOM-элементами.


Архитектура зависимых селектов

Классическая схема зависимости строится по принципу направленного графа:

  • Родительский селект задаёт контекст
  • Дочерний селект обновляется на основе выбранного значения
  • При необходимости цепочка расширяется на несколько уровней

Основные свойства такой структуры:

  • односторонняя зависимость (родитель влияет на потомка)
  • реактивное обновление данных
  • сброс состояния downstream-элементов при изменении upstream

Базовая инициализация Slim Select

Для работы с зависимыми селектами каждый элемент инициализируется отдельно.

<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;
}

Использование кэша особенно эффективно в интерфейсах с повторяющимися переключениями фильтров.


Очистка downstream-цепочки

При любой смене родителя важно синхронно очищать все уровни ниже.

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([]);
}

Производительность при больших списках

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

  • серверная фильтрация вместо клиентской
  • пагинация данных
  • ленивое подгружение (lazy loading)
  • минимизация setData вызовов

Особенно критично избегать частых полных пересборок списка при каждом изменении фильтра.


Типовые ошибки реализации

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

Обобщённый шаблон зависимости

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.