InfiniteLoader, VirtualSelect & VirtualScrollbar
InfiniteLoader
Оборачивает VirtualList и вызывает onLoadMore, когда пользователь прокручивает в пределах threshold пикселей от низа (или верха, либо обоих).
Пропы
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
items | T[] | — | Текущий массив данных |
onLoadMore | () => Promise<void> | — | Вызывается, когда нужны дополнительные данные |
isLoading | boolean | — | Выполняется ли загрузка в данный момент |
hasMore | boolean | — | Доступны ли ещё данные |
threshold | number | 200 | Расстояние от края (px), при котором срабатывает onLoadMore |
direction | 'down' | 'up' | 'both' | 'down' | Какой(-ие) край(-я) запускают загрузку |
estimatedItemSize | number | 50 | Оценочная высота строки |
overscan | number | 3 | Дополнительные строки, рендерящиеся вне вьюпорта |
keyField | string | 'id' | Поле ключа строки |
motionBlur | boolean | false | Применить CSS-размытие, масштабируемое по скорости прокрутки при быстрой прокрутке |
Слоты
| Слот | Область видимости | Описание |
|---|---|---|
#default | { item: T, index: number, style } | Содержимое строки |
#loading-indicator | — | Кастомный индикатор загрузки (показывается сверху/снизу в зависимости от direction) |
#empty | — | Пустое состояние |
Emits
scroll, visible-range-change.
Публичный API
loaderRef.value?.scrollTo(index, align)
loaderRef.value?.scrollTo(index, 'start', { behavior: 'smooth' })
loaderRef.value?.scrollToOffset(px)
loaderRef.value?.getScrollElement()Пример
<script setup lang="ts">
import { ref } from 'vue'
import { InfiniteLoader } from 'vue-virtual-scroller-kit'
interface Post {
id: number
title: string
}
const posts = ref<Post[]>([])
const isLoading = ref(false)
const hasMore = ref(true)
let page = 0
async function loadMore() {
if (isLoading.value || !hasMore.value) return
isLoading.value = true
try {
const res = await fetch(`/api/posts?page=${page}`)
const data: Post[] = await res.json()
posts.value = [...posts.value, ...data]
hasMore.value = data.length === 20
page++
} finally {
isLoading.value = false
}
}
await loadMore()
</script>
<template>
<InfiniteLoader
:items="posts"
:on-load-more="loadMore"
:is-loading="isLoading"
:has-more="hasMore"
:estimated-item-size="72"
style="height: 600px"
>
<template #default="{ item }">
<div class="post-row">{{ item.title }}</div>
</template>
<template #loading-indicator>
<div style="padding: 16px; text-align: center">Loading…</div>
</template>
</InfiniteLoader>
</template>VirtualSelect
Поле выбора с поиском на основе виртуализированного выпадающего списка. Обрабатывает сотни тысяч опций без нагрузки на DOM.
Пропы
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
options | T[] | — | Объекты опций |
modelValue | T | null | null | Текущая выбранная опция |
labelField | string | 'label' | Поле, отображаемое в триггере и выпадающем списке |
valueField | string | 'value' | Поле, используемое для сравнения на равенство |
placeholder | string | 'Select an option…' | Текст плейсхолдера |
disabled | boolean | false | Отключить select |
clearable | boolean | false | Показывать кнопку очистки, когда значение выбрано |
searchable | boolean | true | Показывать поле поиска при открытии выпадающего списка |
estimatedItemSize | number | 36 | Оценочная высота строки опции |
maxVisibleRows | number | 8 | Максимум строк, показываемых до прокрутки списка |
motionBlur | boolean | false | Применить CSS-размытие, масштабируемое по скорости прокрутки при быстрой прокрутке |
remote | boolean | false | Пропустить клиентскую фильтрацию — options рендерится как есть; вы обновляете его сами в ответ на search |
debounceMs | number | 0 | Задержка перед срабатыванием события search после остановки ввода (объединяет нажатия клавиш для запроса к серверу). 0 сохраняет текущее синхронное поведение |
isLoading | boolean | false | Показывает слот #loading в выпадающем списке, проверяется до слота #empty, чтобы выполняющийся удалённый поиск не мигал "No options" |
Emits
| Событие | Payload |
|---|---|
update:modelValue | T | null |
change | T | null |
search | string |
Слоты
| Слот | Область видимости | Описание |
|---|---|---|
#default | { option: T, index: number, selected: boolean } | Кастомная строка опции |
#empty | — | Показывается, когда filteredOptions пуст и загрузки нет |
#loading | — | Показывается, пока isLoading истинно, вместо списка опций |
Публичный API
selectRef.value?.open()
selectRef.value?.close()
selectRef.value?.getScrollElement() // сочетайте с VirtualScrollbarПример
<script setup lang="ts">
import { ref } from 'vue'
import { VirtualSelect } from 'vue-virtual-scroller-kit'
interface Country {
value: string
label: string
flag: string
}
const countries: Country[] = [
{ value: 'us', label: 'United States', flag: '🇺🇸' },
{ value: 'de', label: 'Germany', flag: '🇩🇪' },
// … сотни других
]
const selected = ref<Country | null>(null)
</script>
<template>
<VirtualSelect
v-model="selected"
:options="countries"
label-field="label"
value-field="value"
clearable
style="width: 300px"
>
<template #default="{ option }"> {{ option.flag }} {{ option.label }} </template>
</VirtualSelect>
</template>Асинхронный/удалённый поиск — options заполняется с сервера, фильтрация происходит там, а не на клиенте:
<script setup lang="ts">
import { ref } from 'vue'
import { VirtualSelect } from 'vue-virtual-scroller-kit'
interface Country {
value: string
label: string
}
const selected = ref<Country | null>(null)
const results = ref<Country[]>([])
const isLoading = ref(false)
let requestId = 0
async function onSearch(query: string) {
const id = ++requestId
isLoading.value = true
try {
const res = await fetch(`/api/countries?q=${encodeURIComponent(query)}`)
const data: Country[] = await res.json()
if (id !== requestId) return // более новое нажатие клавиши уже запустило другой запрос
results.value = data
} finally {
if (id === requestId) isLoading.value = false
}
}
</script>
<template>
<VirtualSelect
v-model="selected"
:options="results"
remote
:debounce-ms="300"
:is-loading="isLoading"
label-field="label"
value-field="value"
style="width: 300px"
@search="onSearch"
>
<template #loading>Searching…</template>
</VirtualSelect>
</template>VirtualScrollbar
Темизируемый кастомный оверлей скроллбара. Отделён от useVirtualScroll — он работает с элементом прокрутки, предоставляемым любым из компонентов выше (через getScrollElement()), либо с любым прокручиваемым элементом, переданным напрямую. Рендерит трек + перетаскиваемый ползунок, размер и позиция которого вычисляются из scrollHeight/clientHeight/scrollTop, перетаскивание — через pointer-события.
Пропы
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
target | () => HTMLElement | null | — | Возвращает элемент прокрутки для синхронизации |
orientation | 'vertical' | 'horizontal' | 'vertical' | Ось прокрутки для отслеживания |
minThumbSize | number | 24 | Минимальный размер ползунка в px, чтобы огромный список не сжимал его до неухватываемой полоски |
CSS custom properties
| Свойство | По умолчанию | Описание |
|---|---|---|
--vvsk-scrollbar-size | 10px | Толщина трека/ползунка |
--vvsk-scrollbar-track | transparent | Фон трека |
--vvsk-scrollbar-thumb | rgb(255 255 255 / 25%) | Фон ползунка |
--vvsk-scrollbar-thumb-hover | rgb(255 255 255 / 40%) | Фон ползунка при наведении/перетаскивании |
Пример
<script setup lang="ts">
import { ref } from 'vue'
import { VirtualList, VirtualScrollbar } from 'vue-virtual-scroller-kit'
import type { VirtualListExpose } from 'vue-virtual-scroller-kit'
const listRef = ref<VirtualListExpose | null>(null)
const items = Array.from({ length: 10_000 }, (_, i) => ({ id: i, text: `Row ${i + 1}` }))
</script>
<template>
<div style="position: relative; display: flex; height: 500px">
<VirtualList
ref="listRef"
:items="items"
:estimated-item-size="48"
class="vvsk-scrollbar-hidden"
style="flex: 1"
>
<template #default="{ item }">
<div style="padding: 12px 16px; border-bottom: 1px solid #eee">{{ item.text }}</div>
</template>
</VirtualList>
<VirtualScrollbar :target="() => listRef?.getScrollElement() ?? null" />
</div>
</template>Скрыть нативный скроллбар на связанном контейнере при использовании VirtualScrollbar (опционально — они могут сосуществовать, если нужны оба):
.vvsk-scrollbar-hidden {
scrollbar-width: none; /* Firefox */
}
.vvsk-scrollbar-hidden::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}