Обновление списка в реальном времени

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

В отличие от статического массива, заданного при инициализации, динамический список позволяет:

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

Базовое обновление списка

После создания экземпляра Awesomplete список можно изменить в любой момент.

<input id="cities">

<script>
const input = document.getElementById("cities");

const awesomplete = new Awesomplete(input, {
    minChars: 1
});

awesomplete.list = [
    "Алматы",
    "Астана",
    "Караганда"
];
</script>

Позднее список заменяется новым набором данных:

awesomplete.list = [
    "Москва",
    "Минск",
    "Ташкент"
];

После присвоения нового массива компонент автоматически перестраивает внутренний список подсказок.


Динамическое обновление при вводе

Наиболее распространённый сценарий — изменение данных при каждом вводе символа.

<input id="search">
const input = document.getElementById("search");

const awesomplete = new Awesomplete(input, {
    minChars: 1
});

const database = [
    "JavaScript",
    "Java",
    "Python",
    "PHP",
    "Perl",
    "Rust",
    "Ruby"
];

input.addEventListener("input", () => {

    const value = input.value.toLowerCase();

    const filtered = database.filter(item =>
        item.toLowerCase().includes(value)
    );

    awesomplete.list = filtered;
});

В этом примере:

  1. пользователь вводит текст;
  2. событие input запускает обработчик;
  3. выполняется фильтрация массива;
  4. Awesomplete получает новый список;
  5. подсказки обновляются мгновенно.

Работа с асинхронными данными

Awesomplete особенно полезен при интеграции с сервером.

Получение данных через Fetch API

const input = document.getElementById("users");

const awesomplete = new Awesomplete(input, {
    minChars: 2
});

input.addEventListener("input", async () => {

    const query = input.value;

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

    const response = await fetch(
        `/api/users?q=${encodeURIComponent(query)}`
    );

    const users = await response.json();

    awesomplete.list = users;
});

Сервер может возвращать массив:

[
    "Alex",
    "Alice",
    "Albert"
]

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


Предотвращение избыточных запросов

При быстром вводе нельзя отправлять запрос на сервер после каждого символа. Это создаёт:

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

Для решения используется debounce.


Debounce при обновлении списка

function debounce(callback, delay) {

    let timeout;

    return function(...args) {

        clearTimeout(timeout);

        timeout = setTimeout(() => {
            callback.apply(this, args);
        }, delay);
    };
}

Использование:

const handler = debounce(async () => {

    const query = input.value;

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

    const response = await fetch(
        `/search?q=${encodeURIComponent(query)}`
    );

    const data = await response.json();

    awesomplete.list = data;

}, 300);

input.addEventListener("input", handler);

Теперь запрос отправляется только после паузы во вводе.


Очистка списка при пустом вводе

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

Правильная реализация:

input.addEventListener("input", () => {

    const value = input.value.trim();

    if(value === "") {

        awesomplete.list = [];
        awesomplete.close();

        return;
    }

});

Метод close() скрывает выпадающий список.


Обновление списка объектов

Awesomplete поддерживает не только строки, но и объекты.

awesomplete.list = [
    {
        label: "JavaScript",
        value: "js"
    },
    {
        label: "Python",
        value: "py"
    }
];

Настройка отображения:

const awesomplete = new Awesomplete(input, {

    item: function(item, value) {

        return Awesomplete.ITEM(
            item.label,
            value
        );
    },

    replace: function(item) {
        this.input.value = item.label;
    }
});

Динамическое обновление объектов выполняется аналогично:

awesomplete.list = newData;

Реальное обновление через WebSocket

Awesomplete можно связать с постоянным соединением.

const socket = new WebSocket("wss://example.com");

socket.addEventListener("message", event => {

    const data = JSON.parse(event.data);

    awesomplete.list = data;
});

Такой подход используется в:

  • чатах;
  • биржевых интерфейсах;
  • системах мониторинга;
  • live-поиске;
  • совместной работе пользователей.

Частичное обновление списка

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

const current = awesomplete._list || [];

const updated = [
    ...current,
    "Новый элемент"
];

awesomplete.list = updated;

Однако использование _list нежелательно, так как это внутреннее свойство библиотеки.

Безопаснее хранить данные отдельно:

let items = [
    "HTML",
    "CSS"
];

const awesomplete = new Awesomplete(input);

awesomplete.list = items;

function addItem(value) {

    items.push(value);

    awesomplete.list = items;
}

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

Иногда содержимое одного поля зависит от другого.

Пример: страна и город

<select id="country">
    <option value="kz">Казахстан</option>
    <option value="ru">Россия</option>
</select>

<input id="city">
const cities = {

    kz: [
        "Алматы",
        "Астана",
        "Шымкент"
    ],

    ru: [
        "Москва",
        "Казань",
        "Новосибирск"
    ]
};

const country = document.getElementById("country");

const awesomplete = new Awesomplete(
    document.getElementById("city")
);

country.addEventListener("change", () => {

    const value = country.value;

    awesomplete.list = cities[value];
});

Список городов изменяется в зависимости от выбранной страны.


Автоматическое открытие списка после обновления

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

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

Метод evaluate() инициирует повторную проверку совпадений и открывает выпадающее меню.


Работа с большими объёмами данных

При обновлении тысяч элементов могут появляться:

  • задержки;
  • зависания интерфейса;
  • медленная фильтрация;
  • высокая нагрузка на DOM.

Ограничение количества результатов

const filtered = database
    .filter(item =>
        item.includes(query)
    )
    .slice(0, 20);

awesomplete.list = filtered;

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

Чтобы не отправлять одинаковые запросы повторно, используется кэш.

const cache = {};

input.addEventListener("input", async () => {

    const query = input.value;

    if(cache[query]) {

        awesomplete.list = cache[query];
        return;
    }

    const response = await fetch(`/search?q=${query}`);

    const data = await response.json();

    cache[query] = data;

    awesomplete.list = data;
});

Преимущества:

  • меньше запросов;
  • выше скорость интерфейса;
  • уменьшение нагрузки на сервер.

Отмена устаревших запросов

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

Решение — AbortController.

let controller;

input.addEventListener("input", async () => {

    if(controller) {
        controller.abort();
    }

    controller = new AbortController();

    const signal = controller.signal;

    try {

        const response = await fetch("/search", {
            signal
        });

        const data = await response.json();

        awesomplete.list = data;

    } catch(error) {

        if(error.name !== "AbortError") {
            console.error(error);
        }
    }
});

Отображение состояния загрузки

Во время получения данных полезно показывать пользователю промежуточное состояние.

awesomplete.list = [
    "Загрузка..."
];

После завершения запроса:

awesomplete.list = data;

При ошибке:

awesomplete.list = [
    "Ошибка загрузки"
];

Обновление с пользовательским фильтром

Awesomplete позволяет полностью переопределить механизм поиска.

const awesomplete = new Awesomplete(input, {

    filter: function(text, input) {

        return text.startsWith(input);
    }
});

Теперь после обновления списка будут отображаться только элементы, начинающиеся с введённой строки.


Интеграция с REST API

Типичный сценарий:

input.addEventListener("input", async () => {

    const query = input.value;

    const response = await fetch(
        `https://api.example.com/search?q=${query}`
    );

    const json = await response.json();

    const items = json.results.map(item => item.name);

    awesomplete.list = items;
});

Серверная структура может быть сложной, поэтому данные обычно преобразуются перед передачей в Awesomplete.


Минимизация мерцания интерфейса

Если список обновляется слишком часто, выпадающее меню может визуально «прыгать».

Для уменьшения проблемы применяются:

  • debounce;
  • ограничение количества обновлений;
  • сравнение старого и нового списка;
  • обновление только при изменении данных.

Пример проверки:

function arraysEqual(a, b) {

    return JSON.stringify(a) === JSON.stringify(b);
}
if(!arraysEqual(currentList, newList)) {

    currentList = newList;

    awesomplete.list = newList;
}

Комбинирование локальных и серверных данных

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

const local = [
    "HTML",
    "CSS"
];

const remote = await fetchData();

awesomplete.list = [
    ...local,
    ...remote
];

Обновление через таймер

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

setInterval(async () => {

    const response = await fetch("/popular");

    const data = await response.json();

    awesomplete.list = data;

}, 5000);

Такой механизм подходит для:

  • популярных запросов;
  • статистики;
  • live-данных;
  • постоянно изменяющихся каталогов.

Полное переинициализирование компонента

Иногда проще создать новый экземпляр Awesomplete.

let awesomplete =
    new Awesomplete(input);

Позднее:

awesomplete = new Awesomplete(input, {
    list: newData
});

Такой подход применяется редко, поскольку обновление list обычно эффективнее.


Ошибки при обновлении списка

Потеря фокуса

Если DOM-элемент пересоздаётся, поле ввода может потерять курсор.

Множественные запросы

Без debounce приложение способно генерировать десятки запросов в секунду.

Гонки запросов

Старые ответы могут перезаписывать новые результаты.

Медленный рендер

Большие массивы ухудшают производительность.

Некорректный формат данных

Awesomplete ожидает массив строк либо объектов корректной структуры.

Ошибка:

awesomplete.list = {
    name: "JavaScript"
};

Правильно:

awesomplete.list = [
    "JavaScript"
];

Архитектура real-time автодополнения

Полноценная система обновления подсказок обычно включает:

  1. поле ввода;
  2. debounce;
  3. асинхронный запрос;
  4. кэширование;
  5. отмену предыдущих запросов;
  6. обработку ошибок;
  7. обновление списка;
  8. отображение состояния загрузки;
  9. фильтрацию результатов;
  10. ограничение количества элементов.

Такой подход позволяет построить быстрый, отзывчивый и масштабируемый интерфейс автодополнения на базе Awesomplete.