При использовании автодополнения в связке с удалёнными API ключевой проблемой становится неопределённость времени ответа. Пользователь вводит символы и ожидает мгновенную реакцию, однако сеть, серверная логика и задержки обработки приводят к паузам. В этот момент интерфейс должен ясно сигнализировать, что запрос обрабатывается, иначе возникает ощущение «зависания» компонента.
В контексте Awesomplete это особенно важно, поскольку библиотека изначально ориентирована на лёгкость и мгновенный отклик при работе с локальными массивами. При переходе к асинхронным источникам необходимо самостоятельно внедрять индикацию загрузки, синхронизируя её с жизненным циклом запроса.
Индикация загрузки в связке с Awesomplete строится вокруг трёх состояний:
На уровне DOM это обычно выражается через изменение классов у input-поля или контейнера компонента.
Ключевой принцип: индикатор должен включаться до начала запроса и выключаться строго после завершения обработки результата, включая обработку ошибок.
Самый простой подход — добавление класса состояния загрузки на input-элемент.
const input = document.querySelector("#search");
const awesomplete = new Awesomplete(input, {
minChars: 2,
list: []
});
async function fetchSuggestions(query) {
input.classList.add("is-loading");
try {
const response = await fetch(`/api/suggest?q=${encodeURIComponent(query)}`);
const data = await response.json();
awesomplete.list = data.results;
} catch (e) {
console.error("Ошибка загрузки:", e);
} finally {
input.classList.remove("is-loading");
}
}
CSS для визуализации состояния:
.is-loading {
background-image: url("spinner.svg");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 16px 16px;
}
Этот подход минимален, но уже решает базовую задачу — пользователь видит, что система работает.
Awesomplete не управляет сетью напрямую, поэтому загрузка должна
инициироваться через событие input.
let timeoutId;
input.addEventListener("input", (e) => {
const value = e.target.value;
if (value.length < 2) return;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fetchSuggestions(value);
}, 300);
});
Здесь одновременно решаются две задачи:
После получения данных необходимо передать их в Awesomplete и принудительно обновить список.
awesomplete.list = data.results;
awesomplete.evaluate();
Важно учитывать, что вызов evaluate() заставляет
библиотеку пересчитать совпадения и перерисовать список. Если загрузка
не завершена, это может привести к «скачкам» интерфейса, поэтому
обновление списка должно происходить строго после получения данных.
Использование только класса на input ограничено. Более гибкий вариант — отдельный индикатор загрузки.
<div class="autocomplete-wrapper">
<input id="search" />
<div class="loader" hidden></div>
</div>
const loader = document.querySelector(".loader");
function setLoading(state) {
loader.hidden = !state;
}
async function fetchSuggestions(query) {
setLoading(true);
try {
const response = await fetch(`/api/suggest?q=${query}`);
const data = await response.json();
awesomplete.list = data.results;
} finally {
setLoading(false);
}
}
CSS:
.loader {
position: absolute;
right: 10px;
top: 50%;
width: 14px;
height: 14px;
margin-top: -7px;
border: 2px solid #ccc;
border-top-color: #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
Такой подход отделяет визуальную логику от поля ввода и упрощает масштабирование интерфейса.
При быстром вводе возникает проблема гонки запросов: более ранний запрос может завершиться позже последнего и перезаписать актуальные данные.
Решение — контроль «версии запроса».
let requestId = 0;
async function fetchSuggestions(query) {
const currentId = ++requestId;
setLoading(true);
try {
const response = await fetch(`/api/suggest?q=${query}`);
const data = await response.json();
if (currentId !== requestId) return;
awesomplete.list = data.results;
} finally {
if (currentId === requestId) {
setLoading(false);
}
}
}
Теперь только последний запрос имеет право влиять на UI и индикатор загрузки.
Более современный подход — отмена предыдущих запросов.
let controller;
async function fetchSuggestions(query) {
if (controller) {
controller.abort();
}
controller = new AbortController();
setLoading(true);
try {
const response = await fetch(`/api/suggest?q=${query}`, {
signal: controller.signal
});
const data = await response.json();
awesomplete.list = data.results;
} catch (e) {
if (e.name !== "AbortError") {
console.error(e);
}
} finally {
setLoading(false);
}
}
Этот вариант снижает нагрузку на сеть и сервер, поскольку устаревшие запросы не доходят до завершения.
Awesomplete не предоставляет встроенных событий загрузки, но можно использовать косвенные сигналы:
listevaluate()Пример расширенной обвязки:
function updateList(data) {
awesomplete.list = data;
awesomplete.evaluate();
setLoading(false);
}
input.addEventListener("blur", () => {
setLoading(false);
});
Такое дублирование состояния предотвращает «зависшие» индикаторы при потере фокуса.
При усложнении логики полезно централизовать управление состоянием:
const state = {
loading: false,
setLoading(value) {
this.loading = value;
document.body.classList.toggle("is-loading", value);
}
};
Теперь UI может реагировать на глобальное состояние:
body.is-loading #search {
opacity: 0.8;
}
Этот подход особенно полезен при нескольких полях автодополнения на странице.
Индикация загрузки не должна активироваться при каждом символе, иначе пользователь будет видеть мигающий интерфейс.
function debounce(fn, delay) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), delay);
};
}
const debouncedFetch = debounce(fetchSuggestions, 250);
input.addEventListener("input", (e) => {
if (e.target.value.length < 2) return;
setLoading(true);
debouncedFetch(e.target.value);
});
Важно: индикатор включается до debounce, но выключается только после ответа сервера.
Ошибка сети должна трактоваться как завершение загрузки.
catch (e) {
awesomplete.list = [];
} finally {
setLoading(false);
}
Игнорирование finally приводит к ситуации, когда
индикатор остаётся активным навсегда при сбое соединения.
При задержках более 500–700 мс полезно добавлять задержанный индикатор, чтобы избежать «мигания» при быстрых ответах:
let loadingTimer;
function setLoadingDelayed() {
loadingTimer = setTimeout(() => {
setLoading(true);
}, 300);
}
function clearLoading() {
clearTimeout(loadingTimer);
setLoading(false);
}
Так индикатор появляется только тогда, когда задержка становится заметной пользователю.
При корректной реализации система индикации в связке с Awesomplete должна обеспечивать: