Pagination component improvements (#198)

* Adds page change event (#189)

Adds first and last pagination items

Co-authored-by: gassio <gassiogi@gmail.com>

* feat: Pagination component refactoring

---------

Co-authored-by: gassio <gassioglou@gmail.com>
Co-authored-by: gassio <gassiogi@gmail.com>
This commit is contained in:
Ilya Artamonov
2023-09-21 13:38:23 +03:00
committed by GitHub
parent 7652d98219
commit 1370f1c776
9 changed files with 314 additions and 122 deletions

View File

@@ -5,10 +5,16 @@ import PaginationTableExample from './pagination/examples/PaginationTableExample
import PaginationWithIconsExample from './pagination/examples/PaginationWithIconsExample.vue'; import PaginationWithIconsExample from './pagination/examples/PaginationWithIconsExample.vue';
import PaginationWithCustomTextExample from './pagination/examples/PaginationWithCustomTextExample.vue'; import PaginationWithCustomTextExample from './pagination/examples/PaginationWithCustomTextExample.vue';
import PaginationWithCustomSlice from './pagination/examples/PaginationWithCustomSlice.vue'; import PaginationWithCustomSlice from './pagination/examples/PaginationWithCustomSlice.vue';
import PaginationSlotsExample from './pagination/examples/PaginationSlotsExample.vue';
</script> </script>
# Vue Pagination - Flowbite # Vue Pagination - Flowbite
#### Use the Tailwind CSS pagination element to indicate a series of content across various pages based on multiple styles and sizes
The pagination component can be used to navigate across a series of content and data sets for various pages such as blog posts, products, and more. You can use multiple variants of this component with or without icons and even for paginating table data entries.
## Default pagination ## Default pagination
Use the following list of pagination items based on two sizes powered by Tailwind CSS utility classes to indicate a series of content for your website.
<PaginationExample />
```vue ```vue
<script setup> <script setup>
@@ -18,10 +24,32 @@ import { ref } from 'vue'
const currentPage = ref(1) const currentPage = ref(1)
</script> </script>
<template> <template>
<Pagination v-model="currentPage" :total-pages="100"></Pagination> <Pagination v-model="currentPage" :total-items="100"></Pagination>
<Pagination v-model="currentPage" :total-items="100" large></Pagination>
</template> </template>
``` ```
<PaginationExample />
## Pagination with icons
The following pagination component example shows how you can use SVG icons instead of text to show the previous and next pages.
<PaginationWithIconsExample />
```vue
<script setup>
import { Pagination } from 'flowbite-vue'
import { ref } from 'vue'
const currentPage = ref(1)
</script>
<template>
<Pagination v-model="currentPage" :total-pages="100" show-icons></Pagination>
</template>
```
## Default with custom length ## Default with custom length
You can use your own pages count in the row by passing props: `slice-length` You can use your own pages count in the row by passing props: `slice-length`
@@ -41,7 +69,7 @@ const currentPage = ref(1)
<PaginationWithCustomSlice /> <PaginationWithCustomSlice />
## Pagination with navigation layout ## Previous and next
```vue ```vue
<script setup> <script setup>
@@ -52,7 +80,7 @@ const currentPage = ref(1)
</script> </script>
<template> <template>
<div class="flex items-center justify-center text-center"> <div class="flex items-center justify-center text-center">
<Pagination v-model="currentPage" :total-pages="100" :layout="'navigation'"></Pagination> <Pagination v-model="currentPage" :total-pages="10" :layout="'navigation'"></Pagination>
</div> </div>
</template> </template>
``` ```
@@ -75,34 +103,14 @@ const currentPage = ref(1)
</script> </script>
<template> <template>
<div class="flex items-center justify-center text-center"> <div class="flex items-center justify-center text-center">
<Pagination <Pagination v-model="currentPage" :layout="'table'" :per-page="20" :total-items="998" class="mb-2" />
v-model="currentPage" <Pagination v-model="currentPage" :layout="'table'" :per-page="20" :total-items="998" large />
:layout="'table'"
:per-page="10"
:total-items="998"
></Pagination>
</div> </div>
</template> </template>
``` ```
<PaginationTableExample /> <PaginationTableExample />
## Pagination with icons
```vue
<script setup>
import { Pagination } from 'flowbite-vue'
import { ref } from 'vue'
const currentPage = ref(1)
</script>
<template>
<Pagination v-model="currentPage" :total-pages="100" show-icons></Pagination>
</template>
```
<PaginationWithIconsExample />
## Pagination with custom labels ## Pagination with custom labels
```vue ```vue
@@ -117,3 +125,69 @@ const currentPage = ref(1)
</template> </template>
``` ```
<PaginationWithCustomTextExample /> <PaginationWithCustomTextExample />
## Slots example
<PaginationSlotsExample />
```vue
<script setup>
import { Pagination } from 'flowbite-vue'
import { ref } from 'vue'
const currentPage = ref(1)
</script>
<template>
<Pagination v-model="currentPage" :total-items="100" :show-labels="false">
<template #prev-icon></template>
<template #next-icon></template>
<template v-slot:page-button="{ page, setPage }">
<button
@click="setPage(page)"
class="flex items-center justify-center first:rounded-l-lg last:rounded-r-lg px-3 h-8 ml-0 leading-tight text-gray-500 bg-purple-200 border border-purple-300 hover:bg-purple-300 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
{{ page }}
</button>
</template>
</Pagination>
Current page: {{ currentPage }}
</template>
```
## API
### Props
| Name | Values | Default |
|---------------------------|-------------------------------------|--------------|
| modelValue | `number` | `1` |
| totalPages | `number` | `10` |
| perPage | `number` | `10` |
| totalItems | `number` | `10` |
| layout | `pagination`, `table`, `navigation` | `pagination` |
| showIcons | `boolean` | `false` |
| sliceLength | `number` | `2` |
| previousLabel | `string` | `Previous` |
| nextLabel | `string` | `Next` |
| enableFirstAndLastButtons | `boolean` | `false` |
| showLabels | `boolean` | `true` |
| large | `boolean` | `false` |
### Events
| Name | Description |
|----------------------|----------------------|
| `update:model-value` | Current page changed |
| `page-changed` | Current page changed |
### Slots
| Name | Description |
|--------------|-------------------------|
| start | content before buttons |
| end | content after buttons |
| first-button | first button content |
| last-button | last button content |
| prev-button | previous button content |
| next-button | next button content |
| prev-icon | previous icon slot |
| next-icon | next icon slot |
| page-button | page button slot |

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="vp-raw"> <div class="vp-raw flex flex-col items-center">
<Pagination v-model="currentPage" :total-pages="100"></Pagination> <Pagination v-model="currentPage" :total-items="100" class="mb-2"></Pagination>
<Pagination v-model="currentPage" :total-items="100" large></Pagination>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="vp-raw flex items-center justify-center text-center"> <div class="vp-raw flex items-center justify-center text-center">
<Pagination v-model="currentPage" :total-pages="100" :layout="'navigation'"></Pagination> <Pagination v-model="currentPage" :total-pages="10" :layout="'navigation'"></Pagination>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -0,0 +1,23 @@
<template>
<div class="vp-raw flex flex-col items-center">
<Pagination v-model="currentPage" :total-items="100" :show-labels="false">
<template #prev-icon></template>
<template #next-icon></template>
<template v-slot:page-button="{ page, setPage }">
<button
@click="setPage(page)"
class="flex items-center justify-center first:rounded-l-lg last:rounded-r-lg px-3 h-8 ml-0 leading-tight text-gray-500 bg-purple-200 border border-purple-300 hover:bg-purple-300 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
{{ page }}
</button>
</template>
</Pagination>
Current page: {{ currentPage }}
</div>
</template>
<script lang="ts" setup>
import { Pagination } from '../../../../src/index'
import { ref } from 'vue'
const currentPage = ref<number>(1)
</script>

View File

@@ -1,11 +1,7 @@
<template> <template>
<div class="vp-raw flex items-center justify-center text-center"> <div class="vp-raw flex flex-col items-center justify-center text-center">
<Pagination <Pagination v-model="currentPage" :layout="'table'" :per-page="20" :total-items="998" class="mb-2" />
v-model="currentPage" <Pagination v-model="currentPage" :layout="'table'" :per-page="20" :total-items="998" large />
:layout="'table'"
:per-page="10"
:total-items="998"
></Pagination>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="vp-raw"> <div class="vp-raw flex items-center justify-center">
<Pagination v-model="currentPage" :total-pages="100" :slice-length="4"></Pagination> <Pagination v-model="currentPage" :total-pages="100" :slice-length="4"></Pagination>
</div> </div>
</template> </template>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="vp-raw flex flex-col"> <div class="vp-raw flex flex-col items-center">
<Pagination v-model="currentPage" :total-pages="100" previous-label="<<<" next-label=">>>"></Pagination> <Pagination v-model="currentPage" :total-pages="100" previous-label="<<<" next-label=">>>"></Pagination>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,33 @@
<template> <template>
<div class="vp-raw flex flex-col"> <div class="vp-raw flex flex-col items-center">
<Pagination v-model="currentPage" :total-pages="100" show-icons></Pagination> <Pagination v-model="currentPage" :total-pages="100" :show-labels="false" class="mb-2">
<template #prev-icon>
<span class="sr-only">Previous</span>
<svg class="w-2.5 h-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" />
</svg>
</template>
<template #next-icon>
<span class="sr-only">Next</span>
<svg class="w-2.5 h-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" />
</svg>
</template>
</Pagination>
<Pagination v-model="currentPage" :total-pages="100" :show-labels="false" large>
<template #prev-icon>
<span class="sr-only">Previous</span>
<svg class="w-2.5 h-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" />
</svg>
</template>
<template #next-icon>
<span class="sr-only">Next</span>
<svg class="w-2.5 h-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" />
</svg>
</template>
</Pagination>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -1,6 +1,6 @@
<template> <template>
<nav aria-label="Page navigation example"> <nav aria-label="Navigation">
<div class="text-sm text-gray-700 dark:text-gray-400 mb-2" v-if="layout === 'table'"> <div class="text-gray-700 dark:text-gray-400 mb-2" :class="large ? 'text-base' : 'text-sm'" v-if="layout === 'table'">
Showing Showing
<span class="font-semibold text-gray-900 dark:text-white">{{ startItemsCount }}</span> <span class="font-semibold text-gray-900 dark:text-white">{{ startItemsCount }}</span>
to to
@@ -8,102 +8,146 @@
of of
<span class="font-semibold text-gray-900 dark:text-white">{{ computedTotalItems }}</span> <span class="font-semibold text-gray-900 dark:text-white">{{ computedTotalItems }}</span>
</div> </div>
<ul class="inline-flex -space-x-px"> <div class="inline-flex" :class="large && 'text-base h-10'">
<li> <slot name="start" />
<button
:disabled="isDecreaseDisabled" <slot name="first-button" v-if="enableFirstAndLastButtons">
@click="decreasePage" <button :disabled="isFirstPage" @click="goToFirstPage" :class="getNavigationButtonClasses(1)">First</button>
class="flex items-center py-2 px-3 ml-0 leading-tight text-gray-500 bg-white rounded-l-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" </slot>
>
<svg v-if="showIcons" stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 20 20" aria-hidden="true" class="h-5 w-5" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg> <slot name="prev-button" :disabled="isDecreaseDisabled" :decrease-page="decreasePage">
{{ previousLabel }} <button :disabled="isDecreaseDisabled" @click="decreasePage" :class="getNavigationButtonClasses(modelValue - 1)">
<slot name="prev-icon">
<svg
v-if="showIcons || $slots['prev-icon']"
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 20 20"
aria-hidden="true"
class="h-5 w-5"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</slot>
<template v-if="showLabels">{{ previousLabel }}</template>
</button> </button>
</li> </slot>
<li <slot v-for="index in pagesToDisplay" :key="index" name="page-button" :page="index" :set-page="setPage" :disabled="isSetPageDisabled(index)">
v-for="index in pagesToDisplay" <button :disabled="isSetPageDisabled(index)" @click="setPage(index)" :class="getPageButtonClasses(index === modelValue)">
:key="index"
>
<button
:disabled="isSetPageDisabled(index)"
@click="setPage(index)"
class="w-12 py-2 px-3 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
:class="{'text-blue-600 dark:text-white bg-blue-50 dark:bg-gray-700': index === modelValue}"
>
{{ index }} {{ index }}
</button> </button>
</li> </slot>
<li> <slot name="next-button" :disabled="isIncreaseDisabled" :increase-page="increasePage">
<button <button :disabled="isIncreaseDisabled" @click="increasePage" :class="getNavigationButtonClasses(modelValue + 1)">
:disabled="isIncreaseDisabled" <template v-if="showLabels">
@click="increasePage" {{ nextLabel }}
class="flex items-center py-2 px-3 leading-tight text-gray-500 bg-white rounded-r-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" </template>
> <slot name="next-icon">
{{ nextLabel }} <svg
<svg v-if="showIcons" stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 20 20" aria-hidden="true" class="h-5 w-5" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg> v-if="showIcons || $slots['next-icon']"
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 20 20"
aria-hidden="true"
class="h-5 w-5"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
</slot>
</button> </button>
</li> </slot>
</ul>
<slot name="last-button" v-if="enableFirstAndLastButtons">
<button :disabled="isLastPage" @click="goToLastPage" :class="getNavigationButtonClasses(computedTotalPages)">Last</button>
</slot>
<slot name="end" />
</div>
</nav> </nav>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import type { PropType } from 'vue'
import type { PaginationLayout } from '@/components/Pagination/types' import type { PaginationLayout } from '@/components/Pagination/types'
import { twMerge } from 'tailwind-merge'
const emit = defineEmits(['update:modelValue']) const emit = defineEmits<{
'update:model-value': [page: number]
'page-changed': [page: number]
}>()
interface IPaginationProps {
modelValue?: number
totalPages?: number
perPage?: number
totalItems?: number
layout?: PaginationLayout
showIcons?: boolean
sliceLength?: number
previousLabel?: string
nextLabel?: string
enableFirstAndLastButtons?: boolean
showLabels?: boolean
large?: boolean
}
const props = defineProps({ const props = withDefaults(defineProps<IPaginationProps>(), {
modelValue: { modelValue: 1,
type: Number, totalPages: undefined,
default: 1, perPage: 10,
}, totalItems: 10,
totalPages: { layout: 'pagination',
type: Number, showIcons: false,
default: 1, sliceLength: 2,
}, previousLabel: 'Prev',
perPage: { nextLabel: 'Next',
type: Number, enableFirstAndLastButtons: false,
default: 10, showLabels: true,
}, large: false,
totalItems: {
type: Number,
required: false,
},
layout: {
type: String as PropType<PaginationLayout>, // 'navigation' | 'pagination' | 'table'
default: 'pagination',
},
showIcons: {
type: Boolean,
default: false,
},
sliceLength: {
type: Number,
default: 2,
},
previousLabel: {
type: String,
default: 'Previous',
},
nextLabel: {
type: String,
default: 'Next',
},
}) })
defineSlots<{
const setPage = (index: number) => { /* eslint-disable @typescript-eslint/no-explicit-any */
emit('update:modelValue', index) start: any
'first-button': any
'prev-button': any
'prev-icon': any
'page-button': any
'next-button': any
'next-icon': any
'last-button': any
end: any
/* eslint-enable @typescript-eslint/no-explicit-any */
}>()
function setPage(index: number) {
emit('update:model-value', index)
emit('page-changed', index)
} }
const decreasePage = () => { function decreasePage() {
emit('update:modelValue', props.modelValue - 1) emit('update:model-value', props.modelValue - 1)
emit('page-changed', props.modelValue - 1)
} }
const increasePage = () => { function increasePage() {
emit('update:modelValue', props.modelValue + 1) emit('update:model-value', props.modelValue + 1)
emit('page-changed', props.modelValue + 1)
}
function goToFirstPage() {
emit('update:model-value', 1)
emit('page-changed', 1)
}
function goToLastPage() {
const lastPage = computedTotalPages.value
emit('update:model-value', lastPage)
emit('page-changed', lastPage)
} }
const computedTotalPages = computed(() => { const computedTotalPages = computed(() => {
if (!props.totalItems) return props.totalPages if (props.totalPages) return props.totalPages
if (!props.perPage) return props.totalPages
return Math.ceil(props.totalItems / props.perPage) return Math.ceil(props.totalItems / props.perPage)
}) })
@@ -147,7 +191,6 @@ const pagesToDisplay = computed(() => {
return pages return pages
}) })
const startItemsCount = computed(() => props.modelValue * props.perPage - props.perPage + 1) const startItemsCount = computed(() => props.modelValue * props.perPage - props.perPage + 1)
const endItemsCount = computed(() => { const endItemsCount = computed(() => {
const count = props.modelValue * props.perPage + 1 const count = props.modelValue * props.perPage + 1
@@ -159,4 +202,32 @@ const computedTotalItems = computed(() => {
if (props.totalItems) return props.totalItems if (props.totalItems) return props.totalItems
return computedTotalPages.value * props.perPage return computedTotalPages.value * props.perPage
}) })
const isFirstPage = computed(() => props.modelValue === 1)
const isLastPage = computed(() => props.modelValue === computedTotalPages.value)
function getPageButtonClasses(active: boolean) {
const baseClasses =
'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
const activeClasses = 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:bg-gray-700 dark:text-white'
const largeClasses = 'px-4 h-10'
return twMerge(baseClasses, active && activeClasses, props.large && largeClasses)
}
function getNavigationButtonClasses(toPage: number) {
const baseClasses =
'flex items-center justify-center first:rounded-l-lg last:rounded-r-lg px-3 h-8 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
const disabledClasses = 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-white cursor-not-allowed'
const largeClasses = 'px-4 h-10'
const tableClasses =
'border-none text-white hover:text-white bg-gray-800 rounded-none first:rounded-l last:rounded-r hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
return twMerge(
baseClasses,
toPage === props.modelValue && disabledClasses,
props.large && largeClasses,
(toPage > computedTotalPages.value || toPage < 1) && disabledClasses,
props.layout === 'navigation' && 'first:mr-3',
(props.layout === 'navigation' || props.layout === 'table') && 'rounded-lg',
props.layout === 'table' && tableClasses,
)
}
</script> </script>