Сценарий серверного автодополнения в Choices.js строится вокруг идеи динамической подгрузки списка вариантов по мере ввода текста пользователем. Вместо предзагруженного массива данных используется API, которое возвращает результаты поиска на основе строки запроса.
Ключевая особенность реализации — управление состоянием списка вручную: очистка текущих значений, отображение индикатора загрузки, обработка пустых ответов и защита от гонки запросов.
Типовой поток работы включает следующие этапы:
Инициализация экземпляра:
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 обеспечивает корректное
управление параллельными запросами.
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 и кэширования обеспечивает
стабильную работу автодополнения даже при нестабильном соединении и
больших объёмах данных.
Для серверного сценария достаточно ограниченного набора методов:
setChoices() — установка данныхclearChoices() — очистка спискаremoveActiveItems() — сброс выбранных значенийdisable() / enable() — управление
состоянием поляТакая модель минимизирует зависимость от внутреннего поиска библиотеки и полностью переносит логику на серверную сторону.