Пагинация в контексте Tom Select чаще всего реализуется не на уровне самой библиотеки, а на уровне источника данных, когда результаты подгружаются частями через AJAX. Такой подход необходим при работе с большими наборами данных, где загрузка полного списка вариантов невозможна или неэффективна.
Основная модель Tom Select предполагает асинхронную функцию загрузки:
new TomSelect('#select', {
valueField: 'id',
labelField: 'title',
searchField: 'title',
load: function(query, callback) {
fetch(`/api/items?q=${encodeURIComponent(query)}`)
.then(res => res.json())
.then(json => {
callback(json.items);
})
.catch(() => callback());
}
});
В этом варианте данные загружаются без пагинации — сервер возвращает весь набор, соответствующий запросу. Однако при росте данных такой подход становится неприемлемым, и вводится постраничная загрузка.
Пагинация в Tom Select строится вокруг двух ключевых принципов:
Серверная часть обычно возвращает структуру вида:
{
"items": [
{ "id": 1, "title": "Alpha" },
{ "id": 2, "title": "Beta" }
],
"has_more": true,
"next_page": 2
}
Наиболее читаемый вариант — использование номера страницы.
new TomSelect('#select', {
valueField: 'id',
labelField: 'title',
searchField: 'title',
load: function(query, callback) {
const self = this;
self.page = self.page || 1;
self.query = self.query || null;
if (self.query !== query) {
self.page = 1;
self.query = query;
self.clearOptions();
}
fetch(`/api/items?q=${encodeURIComponent(query)}&page=${self.page}`)
.then(res => res.json())
.then(data => {
callback(data.items);
self.page++;
self.hasMore = data.has_more;
})
.catch(() => callback());
}
});
self.page хранит текущую страницуhasMore контролирует возможность дальнейшей
загрузкиАльтернативный и часто более предсказуемый способ — использование смещения.
new TomSelect('#select', {
valueField: 'id',
labelField: 'title',
searchField: 'title',
load: function(query, callback) {
const self = this;
self.offset = self.offset || 0;
self.limit = 20;
self.query = self.query || null;
if (self.query !== query) {
self.offset = 0;
self.query = query;
self.clearOptions();
}
fetch(`/api/items?q=${encodeURIComponent(query)}&offset=${self.offset}&limit=${self.limit}`)
.then(res => res.json())
.then(data => {
callback(data.items);
self.offset += data.items.length;
self.hasMore = data.has_more;
})
.catch(() => callback());
}
});
Tom Select не навязывает автоматическую пагинацию интерфейса, поэтому механизм “догрузки” реализуется вручную.
Типичный паттерн — загрузка следующей страницы при достижении конца списка результатов:
onDropdownOpen: function() {
const self = this;
const loadMoreIfNeeded = () => {
if (!self.hasMore) return;
const dropdown = self.dropdown_content;
const scrollPosition = dropdown.scrollTop + dropdown.clientHeight;
const scrollHeight = dropdown.scrollHeight;
if (scrollPosition >= scrollHeight - 20) {
self.load(self.lastQuery, () => {});
}
};
self.dropdown_content.addEventListener('scroll', loadMoreIfNeeded);
}
При постраничной загрузке важно не затирать ранее загруженные
элементы. Tom Select ожидает, что callback() будет получать
только новые элементы, а не полный список.
callback(data.items);
Не допускается:
callback([...oldItems, ...newItems]);
Старая коллекция управляется самим Tom Select.
При быстром вводе текста возможны гонки запросов, когда ответы приходят в неверном порядке. Для пагинации это особенно критично.
load: function(query, callback) {
const self = this;
self.requestId = (self.requestId || 0) + 1;
const currentRequest = self.requestId;
fetch(`/api/items?q=${query}`)
.then(res => res.json())
.then(data => {
if (currentRequest !== self.requestId) return;
callback(data.items);
});
}
При повторном вводе одинакового запроса полезно кешировать уже загруженные страницы.
self.cache = self.cache || {};
const key = `${query}:${self.page}`;
if (self.cache[key]) {
callback(self.cache[key]);
return;
}
self.cache[key] = data.items;
Кеширование снижает количество запросов и ускоряет работу интерфейса.
При добавлении сортировки важно учитывать, что изменение порядка данных требует сброса пагинации.
if (self.sort !== newSort) {
self.page = 1;
self.offset = 0;
self.clearOptions();
self.cache = {};
}
Сортировка должна быть частью ключа запроса:
const key = `${query}:${sort}:${page}`;
Хотя основное внимание уделяется клиенту, корректная серверная реализация критична.
app.get('/api/items', (req, res) => {
const q = req.query.q || '';
const page = parseInt(req.query.page || 1);
const limit = 20;
const filtered = database.filter(item =>
item.title.toLowerCase().includes(q.toLowerCase())
);
const start = (page - 1) * limit;
const end = start + limit;
res.json({
items: filtered.slice(start, end),
has_more: end < filtered.length
});
});
При пагинации возможна ситуация, когда следующая страница пуста, но
has_more ещё не обновился корректно.
if (!data.items.length) {
self.hasMore = false;
return;
}
Пагинация часто сочетается с debounce логикой ввода.
loadThrottle: 300
или ручная реализация:
let timer;
load: function(query, callback) {
clearTimeout(timer);
timer = setTimeout(() => {
fetch(`/api/items?q=${query}`)
.then(res => res.json())
.then(data => callback(data.items));
}, 300);
}
Наиболее сложный, но распространённый сценарий:
self.state = {
query: '',
page: 1,
hasMore: true,
loading: false
};
if (self.state.loading || !self.state.hasMore) return;
self.state.loading = true;
has_moreЧасто требуется отдельная логика, когда запрос пустой:
if (!query.length) {
callback([]);
return;
}
или загрузка популярных элементов:
fetch('/api/items?popular=1')
Пагинация в Tom Select строится вокруг трёх уровней:
loadТакой подход позволяет масштабировать выборки до десятков и сотен тысяч элементов без деградации интерфейса и без перегрузки браузера.