Статический массив значений подходит только для небольших списков, которые известны заранее. В реальных приложениях автодополнение чаще всего получает данные с сервера: из базы данных, REST API, поискового индекса, CRM-системы или внешнего сервиса.
Динамическая загрузка позволяет:
Awesomplete не содержит встроенного AJAX-механизма, однако библиотека
легко интегрируется с fetch, XMLHttpRequest,
axios и любыми другими средствами HTTP-запросов.
Общий алгоритм работы выглядит следующим образом:
input.awesomplete.list.<input id="cities">
const input = document.querySelector("#cities");
const awesomplete = new Awesomplete(input);
input.addEventListener("input", async () => {
const query = input.value;
if (query.length < 2) {
awesomplete.list = [];
return;
}
const response = await fetch(
`/api/cities?q=${encodeURIComponent(query)}`
);
const data = await response.json();
awesomplete.list = data;
});
const query = input.value;
Считывается текущее содержимое поля ввода.
if (query.length < 2)
Минимальная длина запроса уменьшает:
Очень короткие запросы обычно дают слишком много результатов.
fetch(`/api/cities?q=${encodeURIComponent(query)}`)
На сервер отправляется поисковая строка.
Пример запроса:
/api/cities?q=mos
const data = await response.json();
Обычно сервер возвращает JSON-массив:
[
"Moscow",
"Mostar",
"Mosul"
]
awesomplete.list = data;
После присвоения нового массива Awesomplete автоматически перестраивает список подсказок.
app.get("/api/cities", (req, res) => {
const query = req.query.q.toLowerCase();
const cities = [
"Moscow",
"Madrid",
"Milan",
"Munich",
"Mostar"
];
const result = cities.filter(city =>
city.toLowerCase().includes(query)
);
res.json(result);
});
Сервер редко возвращает простые строки. Обычно используются объекты.
[
{
"id": 1,
"name": "JavaScript"
},
{
"id": 2,
"name": "Java"
}
]
const awesomplete = new Awesomplete(input, {
list: [],
replace(item) {
this.input.value = item.name;
}
});
input.addEventListener("input", async () => {
const response = await fetch(
`/api/tags?q=${input.value}`
);
const data = await response.json();
awesomplete.list = data;
});
Awesomplete должен понимать:
Для этого используются функции data, item,
replace.
const awesomplete = new Awesomplete(input, {
data(item) {
return {
label: item.name,
value: item
};
},
replace(item) {
this.input.value = item.value.name;
}
});
Функция преобразует объект сервера во внутренний формат Awesomplete.
{
label: item.name,
value: item
}
Текст подсказки.
Исходный объект.
Сервер может возвращать:
[
{
"id": 10,
"name": "JavaScript",
"category": "Programming",
"icon": "js.png"
}
]
Эти данные можно использовать при рендеринге.
const awesomplete = new Awesomplete(input, {
item(item, input) {
const element = document.createElement("li");
element.innerHTML = `
<strong>${item.value.name}</strong>
<small>${item.value.category}</small>
`;
return element;
}
});
При вводе текста событие input вызывается после каждого
символа.
Например:
m
mo
mos
mosc
Это может привести к десяткам запросов в секунду.
Debounce ограничивает частоту вызовов функции.
function debounce(callback, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}
const loadData = debounce(async () => {
const query = input.value;
if (query.length < 2) {
return;
}
const response = await fetch(
`/api/search?q=${query}`
);
const data = await response.json();
awesomplete.list = data;
}, 300);
input.addEventListener("input", loadData);
Если пользователь продолжает ввод:
j
ja
jav
java
таймер постоянно перезапускается.
Запрос выполняется только после паузы.
Без отмены может возникнуть проблема гонки запросов.
"ja"."java"."java" приходит быстрее."ja" и перезаписывает список.В результате отображаются устаревшие данные.
Современный способ отмены запросов.
let controller;
input.addEventListener("input", async () => {
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const response = await fetch(
`/api/search?q=${input.value}`,
{
signal: controller.signal
}
);
const data = await response.json();
awesomplete.list = data;
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
}
}
});
Отмена запросов:
Во время загрузки полезно отображать состояние интерфейса.
const loader = document.querySelector(".loader");
input.addEventListener("input", async () => {
loader.style.display = "block";
try {
const response = await fetch(
`/api/search?q=${input.value}`
);
const data = await response.json();
awesomplete.list = data;
} finally {
loader.style.display = "none";
}
});
Сетевые ошибки возникают регулярно:
input.addEventListener("input", async () => {
try {
const response = await fetch(
`/api/search?q=${input.value}`
);
if (!response.ok) {
throw new Error("Server error");
}
const data = await response.json();
awesomplete.list = data;
} catch (error) {
console.error(error);
awesomplete.list = [];
}
});
Повторные запросы можно сохранять локально.
const cache = {};
input.addEventListener("input", async () => {
const query = input.value;
if (cache[query]) {
awesomplete.list = cache[query];
return;
}
const response = await fetch(
`/api/search?q=${query}`
);
const data = await response.json();
cache[query] = data;
awesomplete.list = data;
});
Кэш:
Типичная серверная схема:
Client
↓
HTTP Request
↓
Backend API
↓
Database / Search Engine
↓
JSON Response
При больших объёмах данных фильтрация должна выполняться на сервере.
Неправильно:
fetch("/api/all-users")
с последующей фильтрацией в браузере.
Правильно:
fetch(`/api/users?q=${query}`)
Большие ответы замедляют интерфейс.
[
"Java",
"JavaScript",
"JavaFX",
"Java EE",
"Java ME"
]
Возврат нескольких тысяч записей.
const result = users
.filter(user =>
user.name.includes(query)
)
.slice(0, 10);
Иногда сервер отвечает медленно.
Awesomplete может открывать старый список, пока новые данные ещё не получены.
Для повышения UX часто используется:
awesomplete.list = [];
const input = document.querySelector("#search");
const awesomplete = new Awesomplete(input);
let controller;
const search = debounce(async () => {
const query = input.value.trim();
if (query.length < 2) {
awesomplete.list = [];
return;
}
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{
signal: controller.signal
}
);
if (!response.ok) {
throw new Error("Request failed");
}
const data = await response.json();
awesomplete.list = data;
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
awesomplete.list = [];
}
}
}, 300);
input.addEventListener("input", search);
Многие проекты используют axios.
input.addEventListener("input", async () => {
const response = await axios.get("/api/search", {
params: {
q: input.value
}
});
awesomplete.list = response.data;
});
Иногда данные нужно загружать только при открытии поля.
input.addEventListener("focus", async () => {
const response = await fetch("/api/popular");
const data = await response.json();
awesomplete.list = data;
awesomplete.evaluate();
});
Метод:
awesomplete.evaluate()
принудительно запускает построение списка.
Это полезно, когда данные были изменены программно.
Часть данных может храниться локально.
const localData = [
"HTML",
"CSS",
"JavaScript"
];
input.addEventListener("input", async () => {
const response = await fetch(
`/api/search?q=${input.value}`
);
const remoteData = await response.json();
awesomplete.list = [
...localData,
...remoteData
];
});
Свойство list можно менять неограниченное количество
раз.
awesomplete.list = data;
Awesomplete автоматически:
При работе с динамическими данными важны:
Снижает количество запросов.
Убирает повторные обращения.
Не перегружает браузер.
Ускоряет рендеринг.
Устраняет race condition.
Снижает нагрузку на backend.
Приводит к лавинообразным запросам.
fetch("/api/users")
Плохо масштабируется.
Может полностью сломать интерфейс.
Вызывает отображение устаревших данных.
Dropdown становится медленным и неудобным.
[
{
"id": 15,
"name": "John Smith",
"email": "john@example.com"
}
]
const awesomplete = new Awesomplete(input, {
data(item) {
return {
label: `${item.name} (${item.email})`,
value: item
};
},
replace(item) {
this.input.value = item.value.name;
this.input.dataset.userId = item.value.id;
}
});
После выбора элемента:
<input
value="John Smith"
data-user-id="15"
>
В поле отображается имя пользователя, а ID сохраняется отдельно.
Современный стандарт для асинхронного кода.
fetch("/api/search")
.then(response => response.json())
.then(data => {
awesomplete.list = data;
});
Код проще масштабировать:
try {
const response = await fetch(url);
const data = await response.json();
awesomplete.list = data;
} catch (error) {
console.error(error);
}
Такой стиль особенно полезен в сложных сценариях с несколькими запросами и дополнительной логикой обработки данных.