Библиотека Awesomplete поддерживает работу с динамическими источниками данных. Вместо заранее подготовленного массива строк список подсказок может загружаться с сервера во время ввода текста. Такой подход особенно важен при работе с большими объёмами данных, поиском по базе пользователей, товарам, городам, тегам, статьям и другим сущностям, количество которых невозможно или нецелесообразно хранить в памяти браузера.
Ajax-запросы позволяют:
Awesomplete не содержит встроенного Ajax-механизма. Библиотека отвечает только за отображение и управление списком подсказок. Загрузка данных реализуется вручную через:
fetch;XMLHttpRequest;axios;jQuery.ajax;Общий алгоритм работы выглядит следующим образом:
input.awesomplete.list.<input id="country-input">
const input = document.getElementById("country-input");
const awesomplete = new Awesomplete(input, {
minChars: 2,
maxItems: 10,
autoFirst: true
});
input.addEventListener("input", async function () {
const query = this.value;
if (query.length < 2) {
awesomplete.list = [];
return;
}
try {
const response = await fetch(
`/countries?q=${encodeURIComponent(query)}`
);
const data = await response.json();
awesomplete.list = data;
} catch (error) {
console.error(error);
}
});
Awesomplete ожидает массив элементов.
[
"Germany",
"Georgia",
"Greece"
]
После получения массива:
awesomplete.list = data;
список автоматически обновляется.
На практике сервер редко возвращает простые строки. Обычно используются объекты.
[
{
"id": 1,
"name": "Germany"
},
{
"id": 2,
"name": "Georgia"
}
]
awesomplete.list = data.map(item => item.name);
Awesomplete поддерживает объекты специального формата.
awesomplete.list = data.map(item => ({
label: item.name,
value: item.id
}));
Теперь:
label отображается в списке;value используется как итоговое значение.Для более сложной логики используется переопределение
replace.
const awesomplete = new Awesomplete(input, {
replace(suggestion) {
input.value = suggestion.label;
input.dataset.id = suggestion.value;
}
});
После выбора:
<input value="Germany" data-id="1">
Без ограничений Ajax-запрос выполняется на каждый символ.
При быстром вводе это создаёт:
Наиболее распространённое решение — debounce.
function debounce(callback, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
callback.apply(this, args);
}, delay);
};
}
const search = debounce(async function () {
const query = input.value;
if (query.length < 2) {
awesomplete.list = [];
return;
}
const response = await fetch(
`/countries?q=${encodeURIComponent(query)}`
);
const data = await response.json();
awesomplete.list = data;
}, 300);
input.addEventListener("input", search);
Теперь запрос выполняется только через 300 мс после остановки ввода.
Проверка количества символов обязательна.
if (query.length < 3) {
return;
}
Если поле очищено, подсказки также должны исчезнуть.
if (!query.trim()) {
awesomplete.list = [];
}
При быстром вводе возможна ситуация:
"ge";"ger";Это называется race condition.
Современный способ решения — AbortController.
let controller;
input.addEventListener("input", async function () {
const query = this.value;
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const response = await fetch(
`/countries?q=${query}`,
{
signal: controller.signal
}
);
const data = await response.json();
awesomplete.list = data;
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
}
}
});
Во время выполнения Ajax-запроса желательно отображать состояние загрузки.
input.classList.add("loading");
После завершения:
input.classList.remove("loading");
let controller;
const awesomplete = new Awesomplete(input);
input.addEventListener("input", async function () {
const query = this.value.trim();
if (query.length < 2) {
awesomplete.list = [];
return;
}
if (controller) {
controller.abort();
}
controller = new AbortController();
input.classList.add("loading");
try {
const response = await fetch(
`/search?q=${encodeURIComponent(query)}`,
{
signal: controller.signal
}
);
const data = await response.json();
awesomplete.list = data;
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
}
} finally {
input.classList.remove("loading");
}
});
Многие приложения используют библиотеку Axios.
input.addEventListener("input", async function () {
const query = this.value;
const response = await axios.get("/countries", {
params: {
q: query
}
});
awesomplete.list = response.data;
});
$("#country-input").on("input", function () {
$.ajax({
url: "/countries",
data: {
q: this.value
},
success(data) {
awesomplete.list = data;
}
});
});
Запросы должны корректно кодировать пользовательский ввод.
encodeURIComponent(query)
"/search?q=" + query
Без кодирования возникают ошибки при:
&, ?, =.Ajax-запросы всегда должны иметь обработчик ошибок.
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Server error");
}
} catch (error) {
console.error(error);
}
Сервер может вернуть пустой массив.
[]
Awesomplete автоматически скроет список.
Иногда требуется собственное сообщение.
if (data.length === 0) {
awesomplete.list = [
"Ничего не найдено"
];
}
Ajax-данные часто требуют сложного отображения.
const awesomplete = new Awesomplete(input, {
item(item, inputValue) {
const element = document.createElement("li");
element.innerHTML = `
<strong>${item.name}</strong>
<small>${item.code}</small>
`;
return element;
}
});
По умолчанию Awesomplete экранирует HTML.
Для ручного управления используется собственный
item.
[
{
"name": "Germany",
"code": "DE"
}
]
item(item) {
const li = document.createElement("li");
li.innerHTML = `
<div class="country-row">
<span>${item.name}</span>
<span>${item.code}</span>
</div>
`;
return li;
}
Awesomplete содержит встроенную функцию:
Awesomplete.ITEM
Можно комбинировать её с Ajax-данными.
item(item, input) {
return Awesomplete.ITEM(
item.name,
input
);
}
Повторные запросы можно сохранять локально.
const cache = {};
if (cache[query]) {
awesomplete.list = cache[query];
return;
}
const response = await fetch(url);
const data = await response.json();
cache[query] = data;
awesomplete.list = data;
Наиболее эффективный подход — фильтрация на сервере.
Иногда сервер возвращает общий список.
Тогда фильтрацию выполняет Awesomplete.
const awesomplete = new Awesomplete(input, {
filter(text, input) {
return text.startsWith(input);
}
});
awesomplete.list = serverData;
Затем:
filter(item, input) {
return item.name.includes(input);
}
Awesomplete легко подключается к REST-сервисам.
fetch("/api/users?search=alex")
[
{
"id": 5,
"username": "alex"
}
]
Иногда API требует токен.
fetch("/api/search", {
headers: {
Authorization: "Bearer TOKEN"
}
});
Не все поисковые API используют GET.
fetch("/search", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
query: input.value
})
});
Иногда данные загружаются только при фокусе.
input.addEventListener("focus", async () => {
if (awesomplete.list.length) {
return;
}
const response = await fetch("/countries");
awesomplete.list = await response.json();
});
Часто комбинируются:
awesomplete.list = [
"JavaScript",
"Python",
"PHP"
];
После ввода:
fetch(...)
Сервер может вернуть повторяющиеся значения.
const unique = [...new Set(data)];
Перед передачей в Awesomplete данные часто преобразуются.
const prepared = data.map(item => ({
label: item.title.trim(),
value: item.id
}));
Не следует отображать сотни подсказок.
maxItems: 8
При работе с Ajax важно учитывать:
Некоторые API возвращают данные порциями.
fetch(`/search?q=${query}&page=1`)
Awesomplete обычно отображает только первую страницу.
Никогда нельзя вставлять HTML из Ajax-ответов без проверки.
Опасный код:
li.innerHTML = item.html;
Если сервер вернёт вредоносный JavaScript, возможен XSS.
li.textContent = item.name;
Современный синтаксис значительно упрощает код.
fetch(url)
.then(response => response.json())
.then(data => {
awesomplete.list = data;
});
const response = await fetch(url);
const data = await response.json();
awesomplete.list = data;
Крупные приложения обычно разделяют:
async function searchCountries(query) {
const response = await fetch(
`/countries?q=${query}`
);
return response.json();
}
input.addEventListener("input", async () => {
const data = await searchCountries(
input.value
);
awesomplete.list = data;
});
Каждое поле может иметь собственный Ajax-источник.
createAutocomplete(
"#countries",
"/api/countries"
);
createAutocomplete(
"#cities",
"/api/cities"
);
function createAutocomplete(selector, url) {
const input = document.querySelector(selector);
const awesomplete = new Awesomplete(input);
input.addEventListener("input", async () => {
const response = await fetch(
`${url}?q=${input.value}`
);
awesomplete.list = await response.json();
});
}