Автодополнение с сервера

Сценарий серверного автодополнения в Choices.js строится вокруг идеи динамической подгрузки списка вариантов по мере ввода текста пользователем. Вместо предзагруженного массива данных используется API, которое возвращает результаты поиска на основе строки запроса.

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


Базовая архитектура интеграции с API

Типовой поток работы включает следующие этапы:

  • пользователь вводит текст в поле
  • отправляется запрос на сервер
  • полученный ответ преобразуется в формат Choices.js
  • список вариантов перерисовывается

Инициализация экземпляра:

import Choices from "choices.js";

const element = document.querySelector("#city-select");

const choices = new Choices(element, {
  searchEnabled: true,
  shouldSort: false,
  removeItemButton: true,
  placeholderValue: "Введите название",
  noResultsText: "Ничего не найдено",
  loadingText: "Загрузка...",
});

На этом этапе Choices.js работает только как UI-обёртка, без собственного набора данных.


Перехват ввода и запуск запроса к серверу

Choices.js не ограничивает способ получения данных, поэтому обработка ввода реализуется через событие изменения строки поиска.

let abortController = null;
let debounceTimer = null;

element.addEventListener("search", (event) => {
  const query = event.detail.value;

  clearTimeout(debounceTimer);

  debounceTimer = setTimeout(() => {
    loadOptions(query);
  }, 300);
});

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


Запрос к серверу с защитой от гонки запросов

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

async function loadOptions(query) {
  if (abortController) {
    abortController.abort();
  }

  abortController = new AbortController();

  try {
    const response = await fetch(`/api/cities?q=${encodeURIComponent(query)}`, {
      signal: abortController.signal,
    });

    const data = await response.json();

    updateChoices(data);
  } catch (err) {
    if (err.name !== "AbortError") {
      console.error("Ошибка загрузки:", err);
    }
  }
}

Использование AbortController обеспечивает корректное управление параллельными запросами.


Преобразование данных API в формат Choices.js

Choices.js ожидает данные в виде массива объектов с полями value и label.

function updateChoices(items) {
  choices.clearChoices();

  const formatted = items.map((item) => ({
    value: item.id,
    label: item.name,
    selected: false,
    disabled: false,
  }));

  choices.setChoices(formatted, "value", "label", true);
}

Четвёртый аргумент true указывает на полную замену текущего списка.


Управление состоянием загрузки

Для улучшения UX важно отображать состояние загрузки.

Choices.js позволяет управлять этим через методы API:

function loadOptions(query) {
  choices.setChoices(
    [{ value: "", label: "Загрузка...", disabled: true }],
    "value",
    "label",
    true
  );

  // далее выполняется fetch
}

После получения ответа список заменяется актуальными данными.


Оптимизация через минимальную длину запроса

Отправка запросов имеет смысл только при достаточной длине строки:

element.addEventListener("search", (event) => {
  const query = event.detail.value;

  if (query.length < 2) {
    choices.clearChoices();
    return;
  }

  loadOptions(query);
});

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


Кэширование результатов поиска

При повторяющихся запросах можно использовать простой кэш в памяти:

const cache = new Map();

async function loadOptions(query) {
  if (cache.has(query)) {
    updateChoices(cache.get(query));
    return;
  }

  if (abortController) abortController.abort();
  abortController = new AbortController();

  const response = await fetch(`/api/cities?q=${query}`, {
    signal: abortController.signal,
  });

  const data = await response.json();
  cache.set(query, data);

  updateChoices(data);
}

Кэш особенно эффективен при автодополнении коротких слов и повторных вводах.


Ограничение количества отображаемых результатов

При больших ответах сервера важно ограничивать число элементов:

function updateChoices(items) {
  const limited = items.slice(0, 20);

  choices.clearChoices();

  choices.setChoices(
    limited.map((item) => ({
      value: item.id,
      label: item.name,
    })),
    "value",
    "label",
    true
  );
}

Это снижает нагрузку на DOM и ускоряет рендеринг.


Работа с пагинацией на сервере

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

async function loadOptions(query, page = 1) {
  const response = await fetch(
    `/api/cities?q=${query}&page=${page}`
  );

  const data = await response.json();

  choices.setChoices(
    data.items.map((item) => ({
      value: item.id,
      label: item.name,
    })),
    "value",
    "label",
    page === 1
  );
}

Параметр page === 1 позволяет решать, очищать список или дополнять его.


Интеграция с бесконечной подгрузкой

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

let currentPage = 1;
let currentQuery = "";

element.addEventListener("search", (event) => {
  currentQuery = event.detail.value;
  currentPage = 1;

  loadOptions(currentQuery, currentPage);
});

document.querySelector(".choices__list").addEventListener("scroll", (e) => {
  const el = e.target;

  if (el.scrollTop + el.clientHeight >= el.scrollHeight) {
    currentPage += 1;
    loadOptions(currentQuery, currentPage);
  }
});

Обработка пустых результатов

Если сервер не возвращает данные, список должен явно отражать это состояние:

function updateChoices(items) {
  if (!items.length) {
    choices.clearChoices();
    choices.setChoices(
      [{ value: "", label: "Нет результатов", disabled: true }],
      "value",
      "label",
      true
    );
    return;
  }

  // обычное обновление
}

Синхронизация значения поля и сервера

При выборе значения важно сохранять консистентность состояния:

element.addEventListener("change", (event) => {
  const value = event.detail.value;
  console.log("Выбранный ID:", value);
});

Серверная модель данных обычно опирается на value, а не на отображаемый текст.


Устойчивость к сетевым задержкам

При высокой задержке сети важна корректная отмена устаревших запросов и защита UI от мигания состояний. Комбинация AbortController, debounce и кэширования обеспечивает стабильную работу автодополнения даже при нестабильном соединении и больших объёмах данных.


Использование минимального набора API Choices.js

Для серверного сценария достаточно ограниченного набора методов:

  • setChoices() — установка данных
  • clearChoices() — очистка списка
  • removeActiveItems() — сброс выбранных значений
  • disable() / enable() — управление состоянием поля

Такая модель минимизирует зависимость от внутреннего поиска библиотеки и полностью переносит логику на серверную сторону.