Индикация загрузки

При использовании автодополнения в связке с удалёнными API ключевой проблемой становится неопределённость времени ответа. Пользователь вводит символы и ожидает мгновенную реакцию, однако сеть, серверная логика и задержки обработки приводят к паузам. В этот момент интерфейс должен ясно сигнализировать, что запрос обрабатывается, иначе возникает ощущение «зависания» компонента.

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


Базовая идея индикации загрузки

Индикация загрузки в связке с Awesomplete строится вокруг трёх состояний:

  • запрос ещё не отправлен
  • запрос отправлен, ответ не получен
  • ответ получен, список обновлён

На уровне DOM это обычно выражается через изменение классов у input-поля или контейнера компонента.

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


Минимальная реализация через CSS-класс

Самый простой подход — добавление класса состояния загрузки на 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 и принудительно обновить список.

awesomplete.list = data.results;
awesomplete.evaluate();

Важно учитывать, что вызов evaluate() заставляет библиотеку пересчитать совпадения и перерисовать список. Если загрузка не завершена, это может привести к «скачкам» интерфейса, поэтому обновление списка должно происходить строго после получения данных.


Улучшенная индикация через отдельный DOM-элемент

Использование только класса на 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 и индикатор загрузки.


Интеграция с AbortController

Более современный подход — отмена предыдущих запросов.

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

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

  • изменение list
  • вызов evaluate()
  • фокус/blur input

Пример расширенной обвязки:

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;
}

Этот подход особенно полезен при нескольких полях автодополнения на странице.


Сочетание debounce и индикации

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

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 приводит к ситуации, когда индикатор остаётся активным навсегда при сбое соединения.


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

При задержках более 500–700 мс полезно добавлять задержанный индикатор, чтобы избежать «мигания» при быстрых ответах:

let loadingTimer;

function setLoadingDelayed() {
    loadingTimer = setTimeout(() => {
        setLoading(true);
    }, 300);
}

function clearLoading() {
    clearTimeout(loadingTimer);
    setLoading(false);
}

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


Итоговая модель поведения

При корректной реализации система индикации в связке с Awesomplete должна обеспечивать:

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