Загрузка опций с сервера

Работа с удалёнными источниками данных в Slim Select строится вокруг динамического наполнения списка опций после инициализации компонента или в момент взаимодействия пользователя с полем. Основная задача такого подхода — минимизировать первоначальный объём данных, ускорить загрузку интерфейса и обеспечить актуальность информации, поступающей с backend-сервера.

Базовый принцип интеграции с API

Slim Select не навязывает строгий механизм загрузки данных с сервера. Вместо этого используется стандартный подход: получение данных через fetch, XMLHttpRequest или любые сторонние HTTP-клиенты с последующей передачей результата в экземпляр селекта.

Структура данных, возвращаемых сервером, как правило, должна быть нормализована:

[
  { "text": "Москва", "value": "moscow" },
  { "text": "Санкт-Петербург", "value": "spb" },
  { "text": "Казань", "value": "kazan" }
]

После получения данных выполняется их преобразование в формат, понятный Slim Select.

Инициализация пустого селекта

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

<select id="citySelect"></select>
const slimSelect = new SlimSelect({
  select: '#citySelect',
  placeholder: 'Выберите значение'
});

На этом этапе список пуст, а данные загружаются отдельно.

Первичная загрузка данных через fetch

Один из наиболее распространённых сценариев — загрузка списка сразу после инициализации:

fetch('/api/cities')
  .then(response => response.json())
  .then(data => {
    slimSelect.setData(
      data.map(item => ({
        text: item.text,
        value: item.value
      }))
    );
  });

Метод setData полностью заменяет текущий список опций, что делает его удобным для первичной инициализации.

Обновление данных при изменении состояния

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

const countrySelect = new SlimSelect({
  select: '#countrySelect',
  onChange: (info) => {
    loadCities(info.value);
  }
});

function loadCities(countryId) {
  fetch(`/api/cities?country=${countryId}`)
    .then(res => res.json())
    .then(data => {
      slimSelect.setData(
        data.map(city => ({
          text: city.name,
          value: city.id
        }))
      );
    });
}

Здесь ключевую роль играет реактивное обновление списка через событие onChange.

Асинхронный поиск и загрузка по вводу

Для больших наборов данных используется стратегия загрузки по мере ввода текста. Такой подход снижает нагрузку на сервер и клиент.

let searchTimeout;

const slimSelect = new SlimSelect({
  select: '#searchSelect',
  searchPlaceholder: 'Введите запрос...',
  onSearch: (query) => {
    clearTimeout(searchTimeout);

    searchTimeout = setTimeout(() => {
      fetch(`/api/search?q=${encodeURIComponent(query)}`)
        .then(res => res.json())
        .then(data => {
          slimSelect.setData(
            data.map(item => ({
              text: item.label,
              value: item.id
            }))
          );
        });
    }, 300);
  }
});

Задержка (debounce) предотвращает избыточные запросы при быстром наборе текста.

Подход с предварительной загрузкой и локальным кэшем

При повторяющихся запросах целесообразно использовать кэширование результатов, чтобы снизить количество обращений к серверу.

const cache = {};

function getCities(query) {
  if (cache[query]) {
    return Promise.resolve(cache[query]);
  }

  return fetch(`/api/cities?q=${query}`)
    .then(res => res.json())
    .then(data => {
      cache[query] = data;
      return data;
    });
}

Далее кэш интегрируется в обработчик поиска:

onSearch: (query) => {
  getCities(query).then(data => {
    slimSelect.setData(
      data.map(i => ({
        text: i.name,
        value: i.id
      }))
    );
  });
}

Частичное обновление списка опций

В некоторых сценариях требуется не полная замена данных, а добавление новых элементов. Для этого используется комбинирование текущего состояния с новыми значениями.

function appendOptions(newItems) {
  const currentData = slimSelect.getData();

  const merged = [
    ...currentData,
    ...newItems.map(i => ({
      text: i.text,
      value: i.value
    }))
  ];

  slimSelect.setData(merged);
}

Такой подход полезен при пагинации или бесконечной прокрутке.

Загрузка данных с пагинацией

При больших объёмах данных сервер может отдавать результаты частями:

let page = 1;
let loading = false;

function loadMore() {
  if (loading) return;
  loading = true;

  fetch(`/api/items?page=${page}`)
    .then(res => res.json())
    .then(data => {
      const formatted = data.items.map(i => ({
        text: i.name,
        value: i.id
      }));

      slimSelect.setData([
        ...slimSelect.getData(),
        ...formatted
      ]);

      page++;
      loading = false;
    });
}

Пагинация особенно эффективна при работе с каталогами и справочниками.

Обработка ошибок при загрузке

Сетевые запросы всегда требуют обработки ошибок, особенно при динамическом наполнении селекта.

fetch('/api/options')
  .then(res => {
    if (!res.ok) {
      throw new Error('Ошибка загрузки');
    }
    return res.json();
  })
  .then(data => {
    slimSelect.setData(data);
  })
  .catch(() => {
    slimSelect.setData([]);
  });

При ошибке список очищается или заменяется fallback-значениями.

Синхронизация состояния с сервером

В некоторых случаях выбранное значение должно немедленно отправляться на backend:

const slimSelect = new SlimSelect({
  select: '#statusSelect',
  onChange: (info) => {
    fetch('/api/save-status', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        status: info.value
      })
    });
  }
});

Такой подход используется в интерфейсах администрирования и CRM-системах.

Динамическое формирование параметров запроса

Запросы к серверу часто зависят от нескольких факторов формы:

function buildQuery() {
  const region = document.querySelector('#region').value;
  const type = document.querySelector('#type').value;

  return `/api/items?region=${region}&type=${type}`;
}
function reloadData() {
  fetch(buildQuery())
    .then(res => res.json())
    .then(data => {
      slimSelect.setData(data);
    });
}

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

Оптимизация частоты запросов

Для снижения нагрузки применяется комбинирование debounce и минимального интервала запросов:

let lastRequestTime = 0;

function canRequest() {
  const now = Date.now();
  if (now - lastRequestTime < 500) return false;
  lastRequestTime = now;
  return true;
}
onSearch: (query) => {
  if (!canRequest()) return;

  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(data => slimSelect.setData(data));
}

Такая защита предотвращает перегрузку backend при активном вводе.

Использование AbortController для отмены запросов

При быстром вводе предыдущие запросы становятся неактуальными и должны быть отменены:

let controller;

function search(query) {
  if (controller) {
    controller.abort();
  }

  controller = new AbortController();

  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(data => {
      slimSelect.setData(data);
    })
    .catch(() => {});
}

Этот механизм повышает стабильность интерфейса при высокой частоте обновлений данных.