Choices.js изначально проектируется как компонент для управления
списками <select> и <input> с
возможностью расширенного поиска и кастомизации. При работе с удалёнными
источниками данных библиотека не содержит встроенного «магического
AJAX-режима» в стиле старых UI-фреймворков — вместо этого используется
модель внешнего управления состоянием через программные методы
экземпляра.
Ключевой принцип асинхронной интеграции заключается в том, что Choices.js не выполняет запросы самостоятельно, а предоставляет API для:
Это делает интеграцию с любым API (REST, GraphQL, JSON-RPC) унифицированной.
Базовая конфигурация предполагает включение поиска и отключение статического набора значений:
const element = document.querySelector('#city-select');
const choices = new Choices(element, {
searchEnabled: true,
shouldSort: false,
placeholderValue: 'Поиск...',
removeItemButton: true
});
На этом этапе компонент готов принимать данные извне, но список опций пока пуст или статичен.
Основной механизм реакции на запросы пользователя — событие
search. Оно позволяет перехватывать ввод и запускать
асинхронный запрос:
element.addEventListener('search', async (event) => {
const query = event.detail.value;
if (query.length < 2) return;
const results = await fetchCities(query);
choices.setChoices(results, 'value', 'label', true);
});
Здесь:
event.detail.value — текущая строка поискаfetchCities — функция обращения к серверуsetChoices — полная замена текущего спискаТиповая реализация AJAX-слоя строится на fetch:
async function fetchCities(query) {
const response = await fetch(`/api/cities?search=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
const data = await response.json();
return data.items.map(item => ({
value: item.id,
label: item.name
}));
}
Ключевая задача слоя преобразования — нормализация ответа API в формат Choices.js:
{
value: 'unique-id',
label: 'Отображаемое название'
}
Метод setChoices поддерживает режимы обновления:
choices.setChoices(data, 'value', 'label', true);
Последний аргумент:
true — полностью очищает старые значенияfalse — добавляет к существующимПри работе с AJAX почти всегда используется полная замена, чтобы избежать дублирования результатов при каждом запросе.
Без ограничения частоты ввода асинхронные запросы могут перегрузить сервер. Используется debounce:
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
Применение:
const handleSearch = debounce(async (value) => {
if (value.length < 2) return;
const data = await fetchCities(value);
choices.setChoices(data, 'value', 'label', true);
}, 300);
element.addEventListener('search', (e) => {
handleSearch(e.detail.value);
});
При быстром вводе возможна гонка ответов, когда старый запрос перезаписывает новый результат. Решение — отмена предыдущего fetch:
let controller = null;
async function fetchCities(query) {
if (controller) {
controller.abort();
}
controller = new AbortController();
const response = await fetch(`/api/cities?search=${query}`, {
signal: controller.signal
});
const data = await response.json();
return data.items.map(item => ({
value: item.id,
label: item.name
}));
}
Это гарантирует актуальность данных в UI.
Для уменьшения нагрузки на сервер используется локальный кэш:
const cache = new Map();
async function fetchCities(query) {
if (cache.has(query)) {
return cache.get(query);
}
const response = await fetch(`/api/cities?search=${query}`);
const data = await response.json();
const formatted = data.items.map(item => ({
value: item.id,
label: item.name
}));
cache.set(query, formatted);
return formatted;
}
При больших данных кэш можно ограничивать по размеру или времени жизни.
Асинхронная интеграция требует явной обработки крайних случаев:
try {
const results = await fetchCities(query);
if (!results.length) {
choices.clearStore();
return;
}
choices.setChoices(results, 'value', 'label', true);
} catch (error) {
choices.clearStore();
}
Метод clearStore используется для очистки списка
опций.
При работе с медленными API важно учитывать UX:
Пример:
element.addEventListener('search', async (e) => {
const query = e.detail.value;
choices.setChoices([{ value: '', label: 'Загрузка...', disabled: true }], 'value', 'label', true);
const data = await fetchCities(query);
choices.setChoices(data, 'value', 'label', true);
});
Иногда сервер возвращает избыточный набор данных, требующий дополнительной фильтрации:
const filtered = data.items
.filter(item => item.population > 100000)
.map(item => ({
value: item.id,
label: item.name
}));
Такая стратегия снижает необходимость в сложных серверных параметрах.
Для больших справочников применяется постраничная загрузка:
let page = 1;
let queryState = '';
async function loadMore(query) {
const response = await fetch(`/api/cities?search=${query}&page=${page}`);
const data = await response.json();
const formatted = data.items.map(item => ({
value: item.id,
label: item.name
}));
choices.setChoices(formatted, 'value', 'label', page === 1);
page++;
queryState = query;
}
При изменении запроса page сбрасывается.
Choices.js в AJAX-режиме часто используется как часть более широкой архитектуры:
Пример синхронизации:
choices.passedElement.element.addEventListener('change', (e) => {
store.setState({
cityId: e.detail.value
});
});
Частая проблема — параллельные вызовы setChoices,
приводящие к «миганию» данных.
Решение — контроль последовательности:
let requestId = 0;
async function handleSearch(query) {
const currentId = ++requestId;
const data = await fetchCities(query);
if (currentId !== requestId) return;
choices.setChoices(data, 'value', 'label', true);
}
Комбинация всех практик:
формирует устойчивую архитектуру асинхронного селекта:
const cache = new Map();
let controller = null;
let requestId = 0;
Эта модель позволяет масштабировать компонент до десятков тысяч записей без деградации UX.