Tom Select при работе в режиме удалённой загрузки данных опирается на строгий контракт между клиентом и сервером. Ключевой элемент этой схемы — формат ответа, который определяет, как библиотека интерпретирует список вариантов, поддерживает фильтрацию, пагинацию и обновление данных без перезагрузки страницы.
Наиболее простой вариант ответа — массив объектов. Каждый объект
представляет один элемент списка и должен содержать как минимум поля,
соответствующие конфигурации valueField и
labelField (по умолчанию value и
label).
[
{ "value": 1, "label": "Москва" },
{ "value": 2, "label": "Санкт-Петербург" }
]
При такой форме Tom Select автоматически преобразует массив в элементы списка, используя заданные поля для отображения и значения.
Если используется нестандартная структура данных, например:
[
{ "id": 1, "title": "Москва" },
{ "id": 2, "title": "Санкт-Петербург" }
]
необходимо явно указать соответствие:
new TomSelect("#select", {
valueField: "id",
labelField: "title",
searchField: "title"
});
Удалённая загрузка активируется через параметр load,
который определяет функцию запроса к серверу при вводе пользователя.
new TomSelect("#select", {
valueField: "id",
labelField: "name",
searchField: "name",
load: function(query, callback) {
fetch(`/api/cities?q=${encodeURIComponent(query)}`)
.then(res => res.json())
.then(data => callback(data))
.catch(() => callback());
}
});
Ключевой момент заключается в том, что callback обязан
быть вызван всегда: либо с данными, либо без аргументов при ошибке. Это
предотвращает зависание интерфейса.
При использовании load сервер может возвращать:
[
{ "id": 10, "name": "Алматы" },
{ "id": 11, "name": "Астана" }
]
{
"items": [
{ "id": 10, "name": "Алматы" },
{ "id": 11, "name": "Астана" }
]
}
В этом случае требуется трансформация:
load: function(query, callback) {
fetch(`/api/cities?q=${query}`)
.then(res => res.json())
.then(json => callback(json.items))
.catch(() => callback());
}
При сложных API почти всегда требуется слой нормализации. Он обеспечивает единый формат независимо от структуры ответа сервера.
function normalizeResponse(json) {
if (Array.isArray(json)) return json;
if (Array.isArray(json.items)) return json.items;
if (Array.isArray(json.data)) return json.data;
return [];
}
Использование:
load: function(query, callback) {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(json => callback(normalizeResponse(json)))
.catch(() => callback());
}
Более читаемый вариант обработки ответа сервера:
load: async function(query, callback) {
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const json = await res.json();
callback(normalizeResponse(json));
} catch (e) {
callback();
}
}
Асинхронная форма упрощает обработку сложных сценариев, включая цепочки запросов и дополнительные проверки.
При работе с удалёнными источниками данных важно учитывать:
Типовая стратегия:
load: function(query, callback) {
fetch(`/api/search?q=${query}`)
.then(res => {
if (!res.ok) throw new Error("Network error");
return res.json();
})
.then(data => callback(normalizeResponse(data)))
.catch(() => {
callback([]);
});
}
Передача пустого массива предпочтительнее undefined,
если требуется явное обновление списка без данных.
Для больших наборов данных сервер часто возвращает частичные результаты.
Пример ответа:
{
"items": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
],
"has_more": true,
"next_page": 2
}
Tom Select не обрабатывает пагинацию автоматически, поэтому логика реализуется вручную:
let currentPage = 1;
new TomSelect("#select", {
load: function(query, callback) {
fetch(`/api/items?q=${query}&page=${currentPage}`)
.then(res => res.json())
.then(json => {
currentPage = json.next_page || currentPage;
callback(json.items);
})
.catch(() => callback());
}
});
Для реализации бесконечной прокрутки используется расширение логики
load и обработка события раскрытия списка.
onDropdownOpen: function() {
if (!this.settings.loadMore) return;
this.load(this.lastQuery, items => {
this.addOption(items);
this.refreshOptions(false);
});
}
Формирование query string играет ключевую роль при интеграции с API:
function buildQuery(query, page) {
const params = new URLSearchParams();
params.set("q", query);
params.set("page", page);
params.set("limit", 20);
return params.toString();
}
Использование:
fetch(`/api/search?${buildQuery(query, 1)}`)
При быстром вводе пользователя возникает проблема гонки запросов.
Решение — AbortController.
let controller = null;
load: function(query, callback) {
if (controller) controller.abort();
controller = new AbortController();
fetch(`/api/search?q=${query}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => callback(data))
.catch(() => callback());
}
Это предотвращает обработку устаревших ответов.
Для оптимизации можно хранить результаты запросов в памяти:
const cache = new Map();
load: function(query, callback) {
if (cache.has(query)) {
callback(cache.get(query));
return;
}
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => {
cache.set(query, data);
callback(data);
})
.catch(() => callback());
}
Кеширование особенно эффективно при повторяющихся запросах пользователей.
Tom Select поддерживает параметр loadThrottle, который
ограничивает частоту вызова load.
new TomSelect("#select", {
loadThrottle: 300
});
Это снижает нагрузку на сервер при быстром вводе текста.
Если сервер возвращает пустой массив, интерфейс должен корректно сбрасывать список:
.then(data => {
const items = normalizeResponse(data);
callback(items.length ? items : []);
})
При необходимости можно возвращать специальные служебные элементы:
[
{ "id": null, "name": "Ничего не найдено", "disabled": true }
]
Сервер может группировать данные:
[
{
"optgroup": "Города",
"items": [
{ "id": 1, "name": "Алматы" }
]
}
]
Требуется трансформация:
function normalizeGroups(data) {
const result = [];
data.forEach(group => {
group.items.forEach(item => {
result.push({
...item,
optgroup: group.optgroup
});
});
});
return result;
}
При обработке серверных ответов важно исключать:
Простейшая защита:
function sanitize(item) {
return {
...item,
label: String(item.label)
.replace(/</g, "<")
.replace(/>/g, ">")
};
}
При масштабных выборках ключевыми становятся:
limitОптимальный контракт:
{
"items": [
{ "id": 1, "name": "A" }
],
"meta": {
"total": 10000
}
}
Клиент в таком случае работает только с текущей страницей данных, не загружая лишнюю информацию.
Полный цикл обработки обычно включает:
callbackload: async function(query, callback) {
try {
if (controller) controller.abort();
controller = new AbortController();
const res = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
});
const json = await res.json();
const data = normalizeResponse(json);
cache.set(query, data);
callback(data);
} catch {
callback();
}
}