GrPagination

Пакет: @feugene/granularityядроГруппа: Навигация

Берут, когда список не помещается на экран.

Когда брать

  • список не помещается на экран — номера страниц с усечением середины;
  • пользователь выбирает размер страницы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 (xslg) читается из GrConfigProvider (componentDefaults.GrPagination.size). Навигационные кнопки берут размер из шкалы GrButton, а не из своей: они обязаны стоять в один ряд с номерами.

Playground 13

Загружается…

Код
<GrPagination />

Установка

npm i @feugene/granularity

Импорт

import { GrPagination } from '@feugene/granularity/components/GrPagination'

API

Props

PropTypeпо умолчаниюОписание
disabledboolean | undefinedfalseГасит всю пагинацию: номера, кнопки, селект размера и поле перехода.
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefinedИмя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать стоит там, где пагинаций на странице несколько и их надо различать.
compactboolean | undefinedfalseКомпактный вариант: вместо нумерованных страниц показывается индикатор «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).
pageSizesnumber[] | undefined[10, 20, 50]
siblingCountnumber | undefined1Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`.
boundaryCountnumber | undefined1Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`.
showJumperboolean | undefinedfalseПоказывать поле «перейти к странице» с быстрым переходом по вводу номера.
showPageSizeboolean | undefinedfalseПоказывать селект размера страницы.
showTotalboolean | undefinedfalseПоказывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее.
jumperLabelstring | undefinedundefinedi18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`.
pageобязательныйnumber
pageSizeобязательныйnumber
totalобязательныйnumber

Slots

SlotTypeОписание
total{ from: number; to: number; total: number; }Диапазон показанных элементов целиком — вместо строки из локали.

Events

EventTypeОписание
update:page[value: number]
update:pageSize[value: number]

Примеры 5

Базовая связка со списком

Минимальный сценарий для GrPagination: меняем страницу, а компонент сам показывает диапазон видимых элементов — проп show-total.

Page 3 Page size 10

Basic Flow
<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 текущая страница выходит за пределы нового количества страниц.

Total 58 Last available page 5 Active page 5

Page Size
<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 1Scaleattention
Customer 2Starterhealthy
Customer 3Scalehealthy
Customer 4Starterattention
Customer 5Scalehealthy
No row action yet

Table Composition
<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) прыгает на страницу с клампингом к диапазону.


Page 7Page size 20Total 482

Compact Jumper
<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: весь блок пагинации меняет масштаб целиком, а не частями.

size="xs"
size="sm"
size="md"
size="lg"

Sizes
<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 — перейти на введённую страницу

Полный клавиатурный контракт пакета

Документация компонентаВсе компоненты