GrPagination
Берут, когда список не помещается на экран.
Когда брать
- список не помещается на экран — номера страниц с усечением середины;
- пользователь выбирает размер страницы —
showPageSizeс набором изpageSizes; - нужно знать объём —
showTotalпечатает «41–60 из 137» рядом с навигацией; - места мало —
compactзаменяет номера индикатором «текущая / всего»; - страниц много —
showJumperдаёт переход по номеру вместо перебора.
Когда взять другое
| Нужно | Берите |
|---|---|
| Список подгружается прокруткой | GrList с virtual и source |
| Строк мало и все помещаются | GrTable |
| Переключаются разделы, а не страницы данных | GrTabs |
| Нужно показать ход длинной операции | GrProgressBar |
page, pageSize и total приходят снаружи: компонент только просит их
изменить. Он ничего не знает про данные и не может знать — их грузит
приложение.
Контролируемый компонент
page, pageSize и total приходят снаружи, компонент только просит их
изменить:
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:total="total"
/>
Именно v-model:page, а не v-model: страница — именованная модель, пропа
modelValue у компонента нет. Промах видно по атрибуту modelvalue на корневом
<div> — необъявленный проп уезжает туда через fallthrough; в dev-режиме
компонент дополнительно печатает предупреждение.
Число страниц считается из total и pageSize. Номера усекаются по алгоритму
boundary/sibling: всегда видны первая и последняя страницы плюс siblingCount
соседей вокруг текущей; boundaryCount задаёт, сколько крайних показывать.
Одиночный пропущенный номер рисуется номером, а не многоточием — ряд не прыгает.
Нечисловой вход подменяется дефолтом: не доехавший page даёт первую
страницу, total — ноль, pageSize — единицу, и каждый случай объясняется
предупреждением в dev-режиме. Без этого undefined разошёлся бы по номерам и
статусу как NaN, а разметка осталась бы правдоподобной на вид.
Страница вне диапазона рендерится зажатой к [1, pageCount]: пока родитель
не подтянул значение, активная кнопка всё равно есть. Когда число страниц
уменьшилось (упал total или вырос pageSize), компонент дополнительно эмитит
update:page с последней доступной страницей — если page живёт в URL или
сторе, эта навигация произойдёт сама.
Что показывать
| Проп | Что добавляет |
|---|---|
showPageSize | селект размера страницы; по умолчанию выключен — базовая пагинация это только номера |
showTotal | «41–60 из 137» слева от навигации |
showJumper | поле «перейти к странице»: Enter или уход фокуса применяют номер, выходящий за диапазон — клампится |
compact | вместо номеров индикатор «текущая / всего» — для мобайла и тулбаров таблиц |
Диапазон целиком заменяется слотом #total — он получает from, to и total:
<GrPagination :total="total" show-total>
<template #total="{ from, to, total }">
Заказы {{ from }}–{{ to }} из {{ total }}
</template>
</GrPagination>
disabled гасит всё разом: номера, кнопки, селект и поле перехода.
Доступность
Корень — role="navigation" с именем из локали (gr.pagination.label). Если
пагинаций на странице две (сверху и снизу таблицы), задайте ariaLabel — иначе
в обзоре диктора будут два одинаковых лендмарка.
Номера лежат в списке (<ul role="list">), поэтому диктор сообщает их
количество; многоточия из него исключены (aria-hidden). Текущая страница
помечена aria-current="page".
Смену страницы объявляет живая область: в компактном режиме её несёт видимый
индикатор, в обычном — скрытая строка «Страница N из M» (gr.pagination.status).
Размеры
size (xs…lg) читается из GrConfigProvider
(componentDefaults.GrPagination.size). Навигационные кнопки берут размер из
шкалы GrButton, а не из своей: они обязаны стоять в один ряд с номерами.
Playground 13
Загружается…
<GrPagination />Установка
npm i @feugene/granularityИмпорт
import { GrPagination } from '@feugene/granularity/components/GrPagination'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать стоит там, где пагинаций на странице несколько и их надо различать. |
compact | boolean | undefined | false | Компактный вариант: вместо нумерованных страниц показывается индикатор «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц). |
pageSizes | number[] | undefined | [10, 20, 50] | — |
siblingCount | number | undefined | 1 | Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. |
boundaryCount | number | undefined | 1 | Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. |
showJumper | boolean | undefined | false | Показывать поле «перейти к странице» с быстрым переходом по вводу номера. |
showPageSize | boolean | undefined | false | Показывать селект размера страницы. |
showTotal | boolean | undefined | false | Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. |
jumperLabel | string | undefined | undefined | i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. |
pageобязательный | number | — | — |
pageSizeобязательный | number | — | — |
totalобязательный | number | — | — |
Slots
| Slot | Type | Описание |
|---|---|---|
total | { from: number; to: number; total: number; } | Диапазон показанных элементов целиком — вместо строки из локали. |
Events
| Event | Type | Описание |
|---|---|---|
update:page | [value: number] | — |
update:pageSize | [value: number] | — |
Примеры 5
Базовая связка со списком
Минимальный сценарий для GrPagination: меняем страницу, а компонент сам показывает диапазон видимых элементов — проп show-total.
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrPagination } from '@feugene/granularity'
const total = ref(137)
const page = ref(3)
const pageSize = ref(10)
</script>
<template>
<div class="grid gap-3">
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" show-total />
<div class="flex flex-wrap gap-2">
<GrBadge>
Page {{ page }}
</GrBadge>
<GrBadge>
Page size {{ pageSize }}
</GrBadge>
</div>
</div>
</template>Смена размера страницы и зажатие номера
Отдельно показываем защиту от типичного UX-багa: когда после смены pageSize текущая страница выходит за пределы нового количества страниц.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrBadge, GrButton, GrPagination } from '@feugene/granularity'
const total = ref(58)
const page = ref(5)
const pageSize = ref(12)
const pageSizes = [6, 12, 24]
const pageCount = computed(() => {
return Math.max(1, Math.ceil(total.value / pageSize.value))
})
function clampPage() {
page.value = Math.min(page.value, pageCount.value)
}
function setTotal(nextTotal: number) {
total.value = nextTotal
clampPage()
}
</script>
<template>
<div class="grid gap-4">
<div class="flex flex-wrap gap-2">
<GrButton size="sm" :variant="total === 58 ? 'primary' : 'outline'" @click="setTotal(58)">
58 items
</GrButton>
<GrButton size="sm" :variant="total === 23 ? 'primary' : 'outline'" @click="setTotal(23)">
23 items
</GrButton>
<GrButton size="sm" :variant="total === 8 ? 'primary' : 'outline'" @click="setTotal(8)">
8 items
</GrButton>
</div>
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
show-page-size
:total="total"
@update:page-size="clampPage"
/>
<div class="flex flex-wrap gap-2">
<GrBadge>
Total {{ total }}
</GrBadge>
<GrBadge>
Last available page {{ pageCount }}
</GrBadge>
<GrBadge>
Active page {{ page }}
</GrBadge>
</div>
</div>
</template>Компонент сознательно не «чинит» внешнее состояние сам — страницу лучше нормализовать в owning-контейнере.
Композиция с GrDataTable
Практический сценарий: GrPagination остаётся контролом навигации, а slicing данных и действия по строкам живут в page-level orchestration.
Actions | |||
|---|---|---|---|
| Customer 1 | Scale | attention | |
| Customer 2 | Starter | healthy | |
| Customer 3 | Scale | healthy | |
| Customer 4 | Starter | attention | |
| Customer 5 | Scale | healthy |
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
GrBadge,
GrButton,
GrDataTable,
GrPagination,
} from '@feugene/granularity'
const page = ref(1)
const pageSize = ref(5)
const pageSizes = [5, 10, 20]
const lastAction = ref('No row action yet')
const rows = Array.from({ length: 18 }, (_, index) => ({
id: index + 1,
customer: `Customer ${index + 1}`,
plan: index % 2 === 0 ? 'Scale' : 'Starter',
status: index % 3 === 0 ? 'attention' : 'healthy',
}))
const columns = [
{ key: 'customer', label: 'Customer', sortable: true },
{ key: 'plan', label: 'Plan', sortable: true },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'actions', label: 'Actions', align: 'right' as const },
]
const pagedRows = computed(() => {
const start = (page.value - 1) * pageSize.value
return rows.slice(start, start + pageSize.value)
})
const pageCount = computed(() => Math.max(1, Math.ceil(rows.length / pageSize.value)))
function clampPage() {
page.value = Math.min(page.value, pageCount.value)
}
</script>
<template>
<div class="grid gap-4">
<GrDataTable :rows="pagedRows" :columns="columns" row-key="id">
<template #cell-status="{ row }">
<GrBadge :tone="row.status === 'healthy' ? 'success' : 'warning'">
{{ row.status }}
</GrBadge>
</template>
<template #cell-actions="{ row }">
<div class="flex justify-end">
<GrButton size="sm" variant="ghost" @click="lastAction = `Opened ${row.customer}`">
Open
</GrButton>
</div>
</template>
</GrDataTable>
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
show-page-size
:total="rows.length"
@update:page-size="clampPage"
/>
<GrBadge>
{{ lastAction }}
</GrBadge>
</div>
</template>Этот пример полезен как recipe: пагинация не знает о таблице, а таблица не знает о page-size логике — связка собирается наверху.
Компактный вид и переход по номеру
Для узких мест (мобайл, тулбары) compact заменяет ряд номеров индикатором «текущая / всего», а show-jumper добавляет поле быстрого перехода: ввод номера + Enter (или blur) прыгает на страницу с клампингом к диапазону.
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrDivider, GrPagination } from '@feugene/granularity'
const total = ref(482)
const page = ref(7)
const pageSize = ref(20)
</script>
<template>
<div class="grid gap-4">
<GrDivider label="compact" align="start" />
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" compact />
<GrDivider label="show-jumper" align="start" />
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" show-jumper />
<GrDivider label="compact + show-jumper" align="start" />
<GrPagination v-model:page="page" v-model:page-size="pageSize" :total="total" compact show-jumper />
<GrDivider />
<div class="flex flex-wrap gap-2">
<GrBadge>Page {{ page }}</GrBadge>
<GrBadge>Page size {{ pageSize }}</GrBadge>
<GrBadge>Total {{ total }}</GrBadge>
</div>
</div>
</template>Jumper клампит ввод к [1, pageCount] и очищает поле после перехода; компонент остаётся контролируемым — страницу двигает v-model:page.
Шкала размеров
Размер доезжает до вложенных GrButton и GrSelect: весь блок пагинации меняет масштаб целиком, а не частями.
<script setup lang="ts">
import { ref } from 'vue'
import { GrPagination } from '@feugene/granularity'
const sizes = ['xs', 'sm', 'md', 'lg'] as const
const page = ref(4)
const pageSize = ref(20)
</script>
<template>
<div class="grid gap-4">
<div v-for="size in sizes" :key="size" class="grid gap-2">
<div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
size="{{ size }}"
</div>
<GrPagination
v-model:page="page"
v-model:page-size="pageSize"
:total="240"
:size="size"
/>
</div>
</div>
</template>Доступность
- Паттерн APG
—- Клавиши
Enter— перейти на введённую страницу