refactor(pagination): pagination internal logic refactored (#2491)

* refactor(pagination): pagination internal logic refactored

* improvement(pagination): improve pr by better naming and misc
This commit is contained in:
ioslh
2021-07-11 13:12:22 +08:00
committed by GitHub
parent 78ef773b36
commit d334142d0d
13 changed files with 604 additions and 558 deletions

View File

@@ -76,6 +76,7 @@ export default {
pagesize: '/page',
total: 'Total {total}',
pageClassifier: '',
deprecationWarning: 'Deprecated usages detected, please refer to the el-pagination documentation for more details',
},
messagebox: {
title: 'Message',

View File

@@ -76,6 +76,7 @@ export default {
pagesize: '条/页',
total: '共 {total} 条',
pageClassifier: '页',
deprecationWarning: '你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档',
},
messagebox: {
title: '提示',

View File

@@ -1,407 +1,371 @@
import { mount } from '@vue/test-utils'
import { sleep } from '@element-plus/test-utils'
import { mount, VueWrapper } from '@vue/test-utils'
import Pagination from '../src/index'
import { nextTick, ref, h } from 'vue'
const TIME_OUT = 100
const assertElementsExistence = (wrapper: VueWrapper<any>, selectors: string[], existence: boolean) => {
selectors.forEach(selector => {
expect(wrapper.find(selector).exists()).toBe(existence)
})
}
describe('Pagination.vue', () => {
test('layout', () => {
const wrapper = mount(Pagination, {
props: {
layout: 'prev, pager, next',
},
const assertCurrent = (wrapper, page) => {
expect(wrapper.find('.el-pager li.active.number').text()).toBe(String(page))
}
const assertPages = (wrapper, total) => {
expect(wrapper.find('.el-pagination .el-pager li:last-child').text()).toBe(String(total))
}
describe('Pagination', () => {
describe('test invalid usages', () => {
const cacheWarn = console.warn
beforeEach(() => {
console.warn = jest.fn()
})
afterEach(() => {
console.warn = cacheWarn
})
test('both absence of total & pageCount is invalid', async () => {
expect(console.warn).not.toHaveBeenCalled()
const total = ref(undefined)
const wrapper = mount({
setup() {
return () => {
return h(Pagination, { total: total.value })
}
},
})
expect(wrapper.find('.el-pagination').exists()).toBe(false)
expect(console.warn).toHaveBeenCalled()
total.value = 100
await nextTick()
expect(wrapper.find('.el-pagination').exists()).toBe(true)
})
test('current-page defined while absence of current-page listener is invalid', () => {
expect(console.warn).not.toHaveBeenCalled()
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 100,
currentPage: 1,
})
}
},
})
expect(wrapper.find('.el-pagination').exists()).toBe(false)
expect(console.warn).toHaveBeenCalled()
})
test('layout with `sizes` restrictions(page-count)', () => {
expect(console.warn).not.toHaveBeenCalled()
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
layout: 'sizes, pager',
pageCount: 10,
})
}
},
})
expect(wrapper.find('.el-pagination').exists()).toBe(false)
expect(console.warn).toHaveBeenCalled()
})
test('layout with `sizes` restrictions(page-size)', () => {
expect(console.warn).not.toHaveBeenCalled()
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
layout: 'sizes, pager',
pageSize: 10,
})
}
},
})
expect(wrapper.find('.el-pagination').exists()).toBe(false)
expect(console.warn).toHaveBeenCalled()
})
expect(wrapper.find('button.btn-prev').exists()).toBe(true)
expect(wrapper.find('ul.el-pager').exists()).toBe(true)
expect(wrapper.find('button.btn-next').exists()).toBe(true)
expect(wrapper.find('.el-pagination__jump').exists()).toBe(false)
expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(false)
expect(wrapper.find('.el-pagination__total').exists()).toBe(false)
})
test('change layout value', async () => {
const layout = ref('prev, pager, next')
const Comp = {
describe('test layout & layout reactive change', () => {
const layoutRef = ref('')
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
class: 'pagination-wrapper',
layout: layout.value,
total: 100,
layout: layoutRef.value,
})
}
},
}
const wrapper = mount(Comp)
expect(wrapper.find('button.btn-prev').exists()).toBe(true)
expect(wrapper.find('ul.el-pager').exists()).toBe(true)
expect(wrapper.find('button.btn-next').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(false)
layout.value = 'prev, pager, next, ->'
await nextTick()
expect(wrapper.find('button.btn-prev').exists()).toBe(true)
expect(wrapper.find('ul.el-pager').exists()).toBe(true)
expect(wrapper.find('button.btn-next').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(false)
layout.value = 'prev, pager, next, ->, jumper'
await nextTick()
expect(wrapper.find('button.btn-prev').exists()).toBe(true)
expect(wrapper.find('ul.el-pager').exists()).toBe(true)
expect(wrapper.find('button.btn-next').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__jump').exists()).toBe(true)
layout.value = 'prev, pager, next, ->, jumper, sizes'
await nextTick()
expect(wrapper.find('button.btn-prev').exists()).toBe(true)
expect(wrapper.find('ul.el-pager').exists()).toBe(true)
expect(wrapper.find('button.btn-next').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__jump').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__sizes').exists()).toBe(true)
layout.value = 'prev, pager, next, ->, jumper, sizes, total'
await nextTick()
expect(wrapper.find('button.btn-prev').exists()).toBe(true)
expect(wrapper.find('ul.el-pager').exists()).toBe(true)
expect(wrapper.find('button.btn-next').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__jump').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__sizes').exists()).toBe(true)
expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__total').exists()).toBe(true)
})
test('slot', () => {
const TestComponent = {
template: `
<el-pagination
layout="slot, prev, pager, next"
:page-size="25"
:total="100">
<span class="slot-test">slot test</span>
</el-pagination>
`,
components: {
'el-pagination': Pagination,
},
}
const wrapper = mount(TestComponent)
expect(wrapper.find('.slot-test').exists()).toBe(true)
})
test('small', () => {
const wrapper = mount(Pagination, {
props: {
small: true,
},
})
expect(wrapper.vm.$el.classList.contains('el-pagination--small')).toBe(true)
})
test('pageSize', () => {
const wrapper = mount(Pagination, {
props: {
pageSize: 25,
total: 100,
},
test('layout empty', async () => {
await nextTick()
expect(wrapper.find('.el-pagination').exists()).toBe(false)
})
expect(wrapper.findAll('li.number').length).toBe(4)
})
test('pageSize: NaN', () => {
const wrapper = mount(Pagination, {
props: {
pageSize: NaN,
total: 100,
},
})
expect(wrapper.findAll('li.number').length).toBe(7)
})
test('pageCount', () => {
const wrapper = mount(Pagination, {
props: {
pageSize: 25,
pagerCount: 5,
pageCount: 50,
},
})
expect(wrapper.findAll('li.number').length).toBe(5)
})
test('pagerCount', () => {
const wrapper = mount(Pagination, {
props: {
pageSize: 25,
total: 1000,
pagerCount: 21,
},
})
expect(wrapper.findAll('li.number').length).toBe(21)
})
test('will work without total & page-count', async () => {
const wrapper = mount(Pagination, {
props: {
pageSize: 25,
currentPage: 2,
},
})
wrapper.find('.btn-prev').trigger('click')
await sleep(TIME_OUT)
expect(wrapper.vm.internalCurrentPage).toEqual(1)
wrapper.find('.btn-prev').trigger('click')
expect(wrapper.vm.internalCurrentPage).toEqual(1)
})
test('currentPage', () => {
const wrapper = mount(Pagination, {
props: {
pageSize: 20,
total: 200,
currentPage: 3,
},
})
expect(wrapper.find('li.number.active').text()).toEqual('3')
})
test('currentPage: NaN', () => {
const wrapper = mount(Pagination, {
props: {
pageSize: 20,
total: 200,
currentPage: NaN,
},
})
expect(wrapper.find('li.number.active').text()).toEqual('1')
expect(wrapper.vm.$el.querySelectorAll('li.number').length).toBe(7)
})
test('layout is empty', () => {
const wrapper = mount(Pagination, {
props: {
layout: '',
},
})
expect(wrapper.vm.$el.textContent).toEqual('')
})
})
describe('click pager', () => {
test('click ul', () => {
const wrapper = mount(Pagination, {
props: {
total: 1000,
},
})
wrapper.find('.el-pager').trigger('click')
expect(wrapper.vm.internalCurrentPage).toEqual(1)
})
test('click li', () => {
const wrapper = mount(Pagination, {
props: {
total: 1000,
},
})
wrapper.findAll('.el-pager li.number')[1].trigger('click')
expect(wrapper.vm.internalCurrentPage).toEqual(2)
})
test('click next icon-more', () => {
const wrapper = mount(Pagination, {
props: {
total: 1000,
},
})
wrapper.find('.btn-quicknext.more').trigger('click')
expect(wrapper.vm.internalCurrentPage).toEqual(6)
})
test('click prev icon-more', async () => {
const wrapper = mount(Pagination, {
props: {
total: 1000,
},
})
wrapper.find('.btn-quicknext.more').trigger('click')
await sleep(TIME_OUT)
expect(wrapper.find('.btn-quickprev.more').exists()).toBe(true)
wrapper.find('.btn-quickprev.more').trigger('click')
expect(wrapper.vm.internalCurrentPage).toEqual(1)
})
test('click last page', async () => {
const wrapper = mount(Pagination, {
props: {
total: 1000,
},
})
const nodes = wrapper.findAll('li.number')
nodes[nodes.length - 1].trigger('click')
await sleep(TIME_OUT)
expect(wrapper.find('.btn-quickprev.more').exists()).toBe(true)
expect(wrapper.find('.btn-quicknext.more').exists()).toBe(false)
})
test('should emit change size evt and update pageSize', async () => {
const onSizeChange = jest.fn()
const wrapper = mount({
components: {
'el-pagination': Pagination,
},
template: `
<el-pagination
popper-class="select-dropdown-klass"
@size-change="onSizeChange"
v-model:page-size="pageSize"
:total="1000"
:page-sizes="[100, 200, 300]"
layout="sizes, pager"
/>
`,
methods: {
onSizeChange,
},
data(){
return {
pageSize: 200,
const layoutSelectorPairs = [
['sizes', '.el-pagination__sizes'],
['prev', 'button.btn-prev'],
['pager', 'ul.el-pager'],
['next', 'button.btn-next'],
['jumper', '.el-pagination__jump'],
['total', '.el-pagination__total'],
]
layoutSelectorPairs.forEach(([layout], idx) => {
test(`layout with only '${layout}'`, async () => {
layoutRef.value = layout
await nextTick()
for(let i = 0; i < layoutSelectorPairs.length; i++) {
expect(wrapper.find(layoutSelectorPairs[i][1]).exists()).toBe(i === idx)
}
},
})
})
const items = document.querySelector('.select-dropdown-klass').querySelectorAll('.el-select-dropdown__item:not(.selected)');
(items[0] as HTMLOptionElement)?.click()
expect(onSizeChange).toHaveBeenCalled()
expect(wrapper.vm.pageSize).toBe(100)
expect(wrapper.findComponent(Pagination).emitted()).toHaveProperty('size-change')
})
test('should handle total size change', async () => {
const onCurrentChange = jest.fn()
const wrapper = mount({
components: {
[Pagination.name]: Pagination,
},
template: `
<el-pagination
:total="total"
:page-size="pageSize"
@current-change="onCurrentChange"
v-model:currentPage="currentPage"
/>
},
`,
methods: {
onCurrentChange,
},
data() {
return {
currentPage: 3,
total: 1000,
pageSize: 100,
}
},
test(`layout with '->, total'`, async () => {
layoutRef.value = '->, total'
await nextTick()
assertElementsExistence(wrapper, ['.el-pagination__total', '.el-pagination__rightwrapper'], true)
})
await nextTick()
test('layout with default layout prop', () => {
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 100,
})
}
},
})
assertElementsExistence(wrapper, [
'.el-pagination__rightwrapper',
'button.btn-prev',
'ul.el-pager',
'button.btn-next',
'.el-pagination__jump',
], true)
})
expect(wrapper.vm.currentPage).toBe(3)
wrapper.vm.total = 100
await nextTick()
expect(wrapper.vm.currentPage).toBe(1)
expect(onCurrentChange).toHaveBeenCalledWith(1)
})
})
test('repeat click next & change current page', async () => {
const onCurrentChange = jest.fn()
const wrapper = mount({
components: {
[Pagination.name]: Pagination,
},
template: `
<el-pagination
:total="total"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
@current-change="onCurrentChange"
v-model:currentPage="currentPage"
/>
},
`,
methods: {
onCurrentChange,
},
data() {
return {
currentPage: 1,
total: 400,
pageSize: 100,
test('test layout with slot', () => {
const TestComponent = {
template: `
<el-pagination
layout="slot, prev, pager, next"
:page-size="25"
:total="100">
<span class="slot-test">slot test</span>
</el-pagination>
`,
components: {
'el-pagination': Pagination,
},
}
},
const wrapper = mount(TestComponent)
expect(wrapper.find('.slot-test').exists()).toBe(true)
})
test('test small layout', () => {
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 100,
small: true,
})
}
},
})
expect(wrapper.vm.$el.classList.contains('el-pagination--small')).toBe(true)
})
test('test with background', async () => {
const withBackground = ref(true)
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 100,
background: withBackground.value,
})
}
},
})
expect(wrapper.find('.is-background').exists()).toBe(true)
withBackground.value = false
await nextTick()
expect(wrapper.find('.is-background').exists()).toBe(false)
})
test('test hide-on-single-page prop', async () => {
const hideOnSinglePage = ref(false)
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 10, // deivded by default page-size(10), there will be only one page
hideOnSinglePage: hideOnSinglePage.value,
})
}
},
})
expect(wrapper.find('.el-pagination').exists()).toBe(true)
hideOnSinglePage.value = true
await nextTick()
expect(wrapper.find('.el-pagination').exists()).toBe(false)
})
})
await nextTick()
describe('test pageSize & currentPage reactive change', () => {
test(`test pageSize change`, async () => {
const pageSize = ref(10)
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
layout: 'pager',
total: 100,
pageSize: pageSize.value,
})
}
},
})
// total pages = Math.ceil(total / pageSize)
assertPages(wrapper, 10)
pageSize.value = 20
await nextTick()
assertPages(wrapper, 5)
pageSize.value = 55
await nextTick()
assertPages(wrapper, 2)
})
test('test currentPage change', async () => {
const pageSize = ref(10)
const defaultCurrentPage = ref(2)
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
layout: 'prev, pager, next',
total: 100,
pageSize: pageSize.value,
defaultCurrentPage: defaultCurrentPage.value,
})
}
},
})
assertCurrent(wrapper, 2)
defaultCurrentPage.value = 1
assertCurrent(wrapper, 2) // still 2
await wrapper.find('.el-pager li:last-child').trigger('click')
assertCurrent(wrapper, 10)
await wrapper.find('button.btn-prev').trigger('click')
assertCurrent(wrapper, 9)
await wrapper.find('button.btn-next').trigger('click')
assertCurrent(wrapper, 10)
pageSize.value = 50
await nextTick()
assertCurrent(wrapper, 2)
})
expect(wrapper.vm.currentPage).toBe(1)
wrapper.find('.btn-next').trigger('click')
await nextTick()
expect(wrapper.vm.currentPage).toBe(2)
wrapper.vm.currentPage = 1
await nextTick()
expect(wrapper.vm.currentPage).toBe(1)
wrapper.find('.btn-next').trigger('click')
await nextTick()
expect(wrapper.vm.currentPage).toBe(2)
})
test('test pageCount change and side effect', async () => {
const pageCount = ref(10)
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
layout: 'prev, pager, next',
pageCount: pageCount.value,
})
}
},
})
assertPages(wrapper, 10)
pageCount.value = 20
await nextTick()
assertPages(wrapper, 20)
await wrapper.find('.el-pager li:last-child').trigger('click')
assertCurrent(wrapper, 20)
pageCount.value = 5
await nextTick()
// side effect, if currentPage is greater than pageCount
// currentPage should change accordingly
assertPages(wrapper, 5)
assertCurrent(wrapper, 5)
})
test('repeat click prev & change current page', async () => {
const onCurrentChange = jest.fn()
const wrapper = mount({
components: {
[Pagination.name]: Pagination,
},
template: `
<el-pagination
:total="total"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
@current-change="onCurrentChange"
v-model:currentPage="currentPage"
/>
},
`,
methods: {
onCurrentChange,
},
data() {
return {
currentPage: 2,
total: 400,
pageSize: 100,
}
},
test('test listener work', async () => {
const pageSizeWatcher = jest.fn()
const currentPageWatcher = jest.fn()
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 100,
layout: 'prev, pager, next, sizes',
['onUpdate:currentPage']: currentPageWatcher,
['onUpdate:pageSize']: pageSizeWatcher,
})
}
},
})
await wrapper.find('.el-pager li:last-child').trigger('click')
assertCurrent(wrapper, 10 /* Math.ceil(100/10) */)
expect(currentPageWatcher).toHaveBeenCalled()
await wrapper.find('.el-select').trigger('click')
await wrapper.getComponent('.el-select-dropdown').find('li:nth-child(2)').trigger('click')
expect(pageSizeWatcher).toHaveBeenCalled()
assertCurrent(wrapper, 5/* Math.ceil(100/20) */)
})
})
await nextTick()
describe('test a11y supports', () => {
test('test a11y attributes', async () => {
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 100,
})
}
},
})
expect(wrapper.find('.el-pagination').attributes('aria-label')).toBe('pagination')
expect(wrapper.find('.el-pagination').attributes('role')).toBe('pagination')
expect(wrapper.find('.el-pagination .btn-prev').attributes('aria-disabled')).toBe('true')
expect(wrapper.find('.el-pagination .btn-next').attributes('aria-disabled')).toBe('false')
expect(wrapper.find('.el-pager li:first-child').attributes('aria-current')).toBe('true')
expect(wrapper.find('.el-pager li:last-child').attributes('aria-current')).toBe('false')
await wrapper.find('.el-pager li:last-child').trigger('click')
expect(wrapper.find('.el-pagination .btn-prev').attributes('aria-disabled')).toBe('false')
expect(wrapper.find('.el-pagination .btn-next').attributes('aria-disabled')).toBe('true')
expect(wrapper.find('.el-pager li:first-child').attributes('aria-current')).toBe('false')
expect(wrapper.find('.el-pager li:last-child').attributes('aria-current')).toBe('true')
})
expect(wrapper.vm.currentPage).toBe(2)
wrapper.find('.btn-prev').trigger('click')
await nextTick()
expect(wrapper.vm.currentPage).toBe(1)
wrapper.vm.currentPage = 2
await nextTick()
expect(wrapper.vm.currentPage).toBe(2)
wrapper.find('.btn-prev').trigger('click')
await nextTick()
expect(wrapper.vm.currentPage).toBe(1)
test('test tabindex interactive', async () => {
const wrapper = mount({
setup() {
return () => {
return h(Pagination, {
total: 100,
})
}
},
})
await wrapper.find('.el-pager li:nth-child(2)').trigger('click')
assertCurrent(wrapper, 2)
await wrapper.find('.el-pager li:nth-child(3)').trigger('click', {
key: 'Enter',
})
assertCurrent(wrapper, 3)
// TODO getComputedStyle is not implemented in jsdom, so I duno how to assert style of psuedo-class
/*
* await wrapper.find('.el-pager li:nth-child(3)').trigger('keyup', {
* key: 'Tab',
* })
* const style = window.getComputedStyle(wrapper.find('.el-pager li:nth-child(4)').element, ':focus-visible')
* expect(style.outline).toBeTruthy()
*/
})
})
})

View File

@@ -1,11 +1,13 @@
import {
defineComponent,
h,
ref,
computed,
watch,
provide,
computed,
defineComponent,
getCurrentInstance,
} from 'vue'
import { warn } from '@element-plus/utils/error'
import { t } from '@element-plus/locale'
import { IPagination } from './pagination'
import Prev from './prev.vue'
@@ -15,10 +17,17 @@ import Jumper from './jumper.vue'
import Total from './total.vue'
import Pager from './pager.vue'
const getValidPageSize = (val: number) => Number.isNaN(val) ? 10 : val
/**
* It it user's responsibility to guarantee that the value of props.total... is number
* (same as pageSize, defaultPageSize, currentPage, defaultCurrentPage, pageCount)
* Otherwise we can reasonable infer that the corresponding field is absent
*/
const isAbsent = v => typeof v !== 'number'
const componentName = 'ElPagination'
export default defineComponent({
name: 'ElPagination',
name: componentName,
components: {
Prev,
@@ -29,14 +38,23 @@ export default defineComponent({
Pager,
},
props: {
pageSize: {
total: {
type: Number,
default: 10,
},
small: Boolean,
pageSize: {
type: Number,
},
total: {
defaultPageSize: {
type: Number,
},
currentPage: {
type: Number,
},
defaultCurrentPage: {
type: Number,
},
@@ -54,11 +72,6 @@ export default defineComponent({
default: 7,
},
currentPage: {
type: Number,
default: 1,
},
layout: {
type: String,
default: 'prev, pager, next, jumper, ->, total',
@@ -86,6 +99,8 @@ export default defineComponent({
default: '',
},
small: Boolean,
background: Boolean,
disabled: Boolean,
@@ -94,201 +109,213 @@ export default defineComponent({
},
emits: [
'update:current-page',
'update:page-size',
// events below are depracated
// v-model:current-page and v-model:page-size are better choices
'size-change',
'current-change',
'prev-click',
'next-click',
'update:currentPage',
'update:pageSize',
],
setup(props, { emit }) {
const lastEmittedPage = ref(-1)
const userChangePageSize = ref(false)
const internalPageSize = ref(getValidPageSize(props.pageSize))
const internalPageCount = computed<Nullable<number>>(() => {
if (typeof props.total === 'number') {
return Math.max(1, Math.ceil(props.total / internalPageSize.value))
} else if (typeof props.pageCount === 'number') {
return Math.max(1, props.pageCount)
setup(props, { emit, slots }) {
const vnodeProps = getCurrentInstance().vnode.props || {}
// we can find @xxx="xxx" props on `vnodeProps` to check if user bind corresponding events
const hasCurrentPageListener = 'onUpdate:currentPage' in vnodeProps || 'onCurrentChange' in vnodeProps
const hasPageSizeListener = 'onUpdate:pageSize' in vnodeProps || 'onSizeChange' in vnodeProps
const assertValidUsage = computed(() => {
// Users have to set either one, otherwise count of pages cannot be determined
if (isAbsent(props.total) && isAbsent(props.pageCount)) return false
// <el-pagination ...otherProps :current-page="xxx" /> without corresponding listener is forbidden now
// Users have to use two way binding of `currentPage`
// If users just want to provide a default value, `defaultCurrentPage` is here for you
if (!isAbsent(props.currentPage) && !hasCurrentPageListener) return false
// When you want to change sizes, things get more complex, detailed below
// Basically the most important value we need is page count
// either directly from props.pageCount
// or calculated from props.total
// we will take props.pageCount precedence over props.total
if (props.layout.includes('sizes')) {
if (!isAbsent(props.pageCount)) {
// if props.pageCount is assign by user, then user have to watch pageSize change
// and recalculate pageCount
if (!hasPageSizeListener) return false
} else if (!isAbsent(props.total)) {
// Otherwise, we will see if user have props.pageSize defined
// If so, meaning user want to have pageSize controlled himself/herself from component
// Thus page size listener is required
// users are account for page size change
if (!isAbsent(props.pageSize)) {
if (!hasPageSizeListener) {
return false
}
} else {
// (else block just for explaination)
// else page size is controlled by el-pagination internally
}
}
}
return null
return true
})
const internalCurrentPage = ref(getValidCurrentPage(props.currentPage))
const innerPageSize = ref(isAbsent(props.defaultPageSize) ? 10 : props.defaultPageSize)
const innerCurrentPage = ref(isAbsent(props.defaultCurrentPage) ? 1 : props.defaultCurrentPage)
function emitChange() {
if (
internalCurrentPage.value !== lastEmittedPage.value ||
userChangePageSize.value
) {
lastEmittedPage.value = internalCurrentPage.value
userChangePageSize.value = false
emit('update:currentPage', internalCurrentPage.value)
emit('current-change', internalCurrentPage.value)
const pageSizeBridge = computed({
get() {
return isAbsent(props.pageSize) ? innerPageSize.value : props.pageSize
},
set(v: number) {
if (isAbsent(props.pageSize)) {
innerPageSize.value = v
}
if (hasPageSizeListener) {
emit('update:page-size', v)
emit('size-change', v)
}
},
})
const pageCountBridge = computed<number>(() => {
let pageCount = 0
if (!isAbsent(props.pageCount)) {
pageCount = props.pageCount
} else if (!isAbsent(props.total)) {
pageCount = Math.max(1, Math.ceil(props.total / pageSizeBridge.value))
}
}
// side effect
if (currentPageBridge.value > pageCount) {
currentPageBridge.value = pageCount
}
return pageCount
})
const currentPageBridge = computed<number>({
get() {
return isAbsent(props.currentPage) ? innerCurrentPage.value : props.currentPage
},
set(v) {
let newCurrentPage = v
if (v < 1) {
newCurrentPage = 1
} else if (v > pageCountBridge.value) {
newCurrentPage = pageCountBridge.value
}
if (isAbsent(props.currentPage)) {
innerCurrentPage.value = newCurrentPage
}
if (hasCurrentPageListener) {
emit('update:current-page', newCurrentPage)
emit('current-change', newCurrentPage)
}
},
})
function handleCurrentChange(val: number) {
internalCurrentPage.value = getValidCurrentPage(val)
userChangePageSize.value = true
emitChange()
currentPageBridge.value = val
}
function handleSizesChange(val: number) {
userChangePageSize.value = true
internalPageSize.value = val
emit('update:pageSize', val)
emit('size-change', val)
function handleSizeChange(val: number) {
pageSizeBridge.value = val
const newPageCount = pageCountBridge.value
if (currentPageBridge.value > newPageCount) {
currentPageBridge.value = newPageCount
}
}
function prev() {
if (props.disabled) return
const newVal = internalCurrentPage.value - 1
internalCurrentPage.value = getValidCurrentPage(newVal)
emit('prev-click', internalCurrentPage.value)
emitChange()
currentPageBridge.value -= 1
emit('prev-click', currentPageBridge.value)
}
function next() {
if (props.disabled) return
const newVal = internalCurrentPage.value + 1
internalCurrentPage.value = getValidCurrentPage(newVal)
emit('next-click', internalCurrentPage.value)
emitChange()
currentPageBridge.value += 1
emit('next-click', currentPageBridge.value)
}
function getValidCurrentPage(value: number | string) {
if (typeof value === 'string') {
value = parseInt(value, 10)
}
let resetValue: number | undefined
if (isNaN(value) || value < 1) {
resetValue = 1
} else if (internalPageCount.value < value){
resetValue = internalPageCount.value
}
return resetValue ?? value
}
watch(() => props.currentPage, val => {
internalCurrentPage.value = getValidCurrentPage(val)
lastEmittedPage.value = internalCurrentPage.value
})
watch(() => props.pageSize, val => {
internalPageSize.value = getValidPageSize(val)
})
watch(
() => internalPageCount.value,
val => {
const oldPage = internalCurrentPage.value
if (val > 0 && oldPage === 0) {
internalCurrentPage.value = 1
} else if (oldPage > val) {
internalCurrentPage.value = val === 0 ? 1 : val
emitChange()
}
},
)
provide<IPagination>('pagination', {
pageCount: computed(() => props.pageCount),
pageCount: pageCountBridge,
disabled: computed(() => props.disabled),
currentPage: computed(() => internalCurrentPage.value),
currentPage: currentPageBridge,
changeEvent: handleCurrentChange,
handleSizesChange,
handleSizeChange,
})
return {
internalCurrentPage,
internalPageSize,
lastEmittedPage,
userChangePageSize,
internalPageCount,
getValidCurrentPage,
emitChange,
handleCurrentChange,
prev,
next,
}
},
render() {
const layout = this.layout
if (!layout) return null
if (this.hideOnSinglePage && this.internalPageCount <= 1) return null
const rootNode = h('div', {
class: [
'el-pagination',
{
'is-background': this.background,
'el-pagination--small': this.small,
},
],
})
const rootChildren = []
const rightWrapperChildren = []
const rightWrapperRoot = h('div', { class: 'el-pagination__rightwrapper' }, rightWrapperChildren)
const TEMPLATE_MAP = {
prev: h(Prev, {
disabled: this.disabled,
currentPage: this.internalCurrentPage,
prevText: this.prevText,
onClick: this.prev,
}),
jumper: h(Jumper),
pager: h(Pager, {
currentPage: this.internalCurrentPage,
pageCount: this.internalPageCount,
pagerCount: this.pagerCount,
onChange: this.handleCurrentChange,
disabled: this.disabled,
}),
next: h(Next, {
disabled: this.disabled,
currentPage: this.internalCurrentPage,
pageCount: this.internalPageCount,
nextText: this.nextText,
onClick: this.next,
}),
sizes: h(Sizes, {
pageSize: this.pageSize,
pageSizes: this.pageSizes,
popperClass: this.popperClass,
disabled: this.disabled,
}),
slot: this.$slots?.default?.() ?? null,
total: h(Total, { total: this.total }),
}
const components = layout.split(',').map((item: string) => item.trim())
let haveRightWrapper = false
components.forEach((c: keyof typeof TEMPLATE_MAP | '->') => {
if (c === '->') {
haveRightWrapper = true
return
return () => {
if (!assertValidUsage.value) {
warn(componentName, t('el.pagination.docRefer'))
return null
}
if (!haveRightWrapper) {
rootChildren.push(TEMPLATE_MAP[c])
} else {
rightWrapperChildren.push(TEMPLATE_MAP[c])
if (!props.layout) return null
if (props.hideOnSinglePage && pageCountBridge.value <= 1) return null
const rootChildren = []
const rightWrapperChildren = []
const rightWrapperRoot = h('div', { class: 'el-pagination__rightwrapper' }, rightWrapperChildren)
const TEMPLATE_MAP = {
prev: h(Prev, {
disabled: props.disabled,
currentPage: currentPageBridge.value,
prevText: props.prevText,
onClick: prev,
}),
jumper: h(Jumper),
pager: h(Pager, {
currentPage: currentPageBridge.value,
pageCount: pageCountBridge.value,
pagerCount: props.pagerCount,
onChange: handleCurrentChange,
disabled: props.disabled,
}),
next: h(Next, {
disabled: props.disabled,
currentPage: currentPageBridge.value,
pageCount: pageCountBridge.value,
nextText: props.nextText,
onClick: next,
}),
sizes: h(Sizes, {
pageSize: pageSizeBridge.value,
pageSizes: props.pageSizes,
popperClass: props.popperClass,
disabled: props.disabled,
}),
slot: slots?.default?.() ?? null,
total: h(Total, { total: isAbsent(props.total) ? 0 : props.total }),
}
})
if (haveRightWrapper && rightWrapperChildren.length > 0) {
rootChildren.unshift(rightWrapperRoot)
const components = props.layout.split(',').map((item: string) => item.trim())
let haveRightWrapper = false
components.forEach((c: keyof typeof TEMPLATE_MAP | '->') => {
if (c === '->') {
haveRightWrapper = true
return
}
if (!haveRightWrapper) {
rootChildren.push(TEMPLATE_MAP[c])
} else {
rightWrapperChildren.push(TEMPLATE_MAP[c])
}
})
if (haveRightWrapper && rightWrapperChildren.length > 0) {
rootChildren.unshift(rightWrapperRoot)
}
return h('div', {
role: 'pagination',
'aria-label': 'pagination',
class: [
'el-pagination',
{
'is-background': props.background,
'el-pagination--small': props.small,
},
],
}, rootChildren)
}
return h(rootNode, {}, rootChildren)
},
})

View File

@@ -3,6 +3,7 @@
type="button"
class="btn-next"
:disabled="internalDisabled"
:aria-disabled="internalDisabled"
@click.self.prevent
>
<span v-if="nextText">{{ nextText }}</span>

View File

@@ -1,9 +1,11 @@
<template>
<ul class="el-pager" @click="onPagerClick">
<ul class="el-pager" @click="onPagerClick" @keyup.enter="onEnter">
<li
v-if="pageCount > 0"
:class="{ active: currentPage === 1, disabled }"
class="number"
:aria-current="currentPage === 1"
tabindex="0"
>
1
</li>
@@ -20,6 +22,8 @@
:key="pager"
:class="{ active: currentPage === pager, disabled }"
class="number"
:aria-current="currentPage === pager"
tabindex="0"
>
{{ pager }}
</li>
@@ -35,6 +39,8 @@
v-if="pageCount > 1"
:class="{ active: currentPage === pageCount, disabled }"
class="number"
:aria-current="currentPage === pageCount"
tabindex="0"
>
{{ pageCount }}
</li>
@@ -142,6 +148,16 @@ export default defineComponent({
}
}
function onEnter(e: UIEvent) {
const target = e.target as HTMLElement
if (target.tagName.toLowerCase() === 'li' && Array.from(target.classList).includes('number')) {
const newPage = Number(target.textContent)
if (newPage !== props.currentPage) {
emit('change', newPage)
}
}
}
function onPagerClick(event: UIEvent) {
const target = event.target as HTMLElement
if (target.tagName.toLowerCase() === 'ul' || props.disabled) {
@@ -180,6 +196,7 @@ export default defineComponent({
pagers,
onMouseenter,
onPagerClick,
onEnter,
}
},
})

View File

@@ -6,7 +6,7 @@ export interface IPagination {
pageCount?: ComputedRef<number>
disabled?: ComputedRef<boolean>
changeEvent?: AnyFunction<any>
handleSizesChange?: AnyFunction<any>
handleSizeChange?: AnyFunction<any>
}
export interface IPaginationProps {

View File

@@ -3,6 +3,7 @@
type="button"
class="btn-prev"
:disabled="internalDisabled"
:aria-disabled="internalDisabled"
@click.self.prevent
>
<span v-if="prevText ">{{ prevText }}</span>

View File

@@ -69,7 +69,7 @@ export default defineComponent({
function handleChange(val: number) {
if (val !== innerPageSize.value) {
innerPageSize.value = val
pagination?.handleSizesChange(Number(val))
pagination?.handleSizeChange(Number(val))
}
}

View File

@@ -278,7 +278,7 @@ $--pagination-line-height-extra-small: $--pagination-height-extra-small !default
cursor: pointer;
box-sizing: border-box;
text-align: center;
margin: 0;
margin: 1px;
&.btn-quicknext,
&.btn-quickprev {
@@ -302,6 +302,10 @@ $--pagination-line-height-extra-small: $--pagination-height-extra-small !default
border-left: 0;
}
&:focus-visible {
outline: 1px solid $--pagination-hover-color;
}
&:hover {
color: $--pagination-hover-color;
}

View File

@@ -144,6 +144,10 @@ button, input, select, textarea {
border-left: #FE6C6F 5px solid;
margin: 20px 0;
ul {
color: #5e6d82;
}
code {
background-color: rgba(255, 255, 255, .7);
color: #445368;

View File

@@ -216,10 +216,12 @@ When there is only one page, hide the pagination by setting the `hide-on-single-
| small | whether to use small pagination | boolean | — | false |
| background | whether the buttons have a background color | boolean | — | false |
| page-size | item count of each page, supports the v-model bidirectional binding | number | — | 10 |
| default-page-size | default initial value of page size | number | - | - |
| total | total item count | number | — | — |
| page-count | total page count. Set either `total` or `page-count` and pages will be displayed; if you need `page-sizes`, `total` is required | number | — | — |
| pager-count | number of pagers. Pagination collapses when the total page count exceeds this value | number | odd number between 5 and 21 | 7 |
| current-page | current page number, supports the v-model bidirectional binding | number | — | 1 |
| default-current-page | default initial value of current-page | number | - | - |
| layout | layout of Pagination, elements separated with a comma | string | `sizes` / `prev` / `pager` / `next` / `jumper` / `->` / `total` / `slot` | 'prev, pager, next, jumper, ->, total' |
| page-sizes | options of item count per page | number[] | — | [10, 20, 30, 40, 50, 100] |
| popper-class | custom class name for the page size Select's dropdown | string | — | — |
@@ -228,6 +230,13 @@ When there is only one page, hide the pagination by setting the `hide-on-single-
| disabled | whether Pagination is disabled | boolean | — | false |
| hide-on-single-page | whether to hide when there's only one page | boolean | — | - |
:::warning
We'll detect some deprecated usages, if your pagination don't appeared or worked as expected, please check rules below:
- You have to define one of `total` and `page-count`, otherwise we can't determine count of total pages.When both defined, `page-count` taken as priority.
- If `current-page` is defined, you have to listen `current-page` change, by also define `@update:current-page`, otherwise pagination didn't work.
- If `page-size` is defined while page size selector displayed(`sizes` included in `layout`), you have to listen `page-size` change as well, by define `@update:page-size`, otherwise change of page size didn't work.
:::
### Events
| Event Name | Description | Parameters |
|---------|--------|---------|
@@ -236,6 +245,10 @@ When there is only one page, hide the pagination by setting the `hide-on-single-
| prev-click | triggers when the prev button is clicked and current page changes | the new current page |
| next-click | triggers when the next button is clicked and current page changes | the new current page |
:::warning
Events above are not recommended(but are still supported for compatible reason), better chioce is to use the two-way data binding by `v-model`.
:::
### Slots
| Name | Description |
| --- | --- |

View File

@@ -216,10 +216,12 @@
| small | 是否使用小型分页样式 | boolean | — | false |
| background | 是否为分页按钮添加背景色 | boolean | — | false |
| page-size | 每页显示条目个数,支持 v-model 双向绑定 | number | — | 10 |
| default-page-size | 每页显示条目数的初始值;| number | - | - |
| total | 总条目数 | number | — | — |
| page-count | 总页数total 和 page-count 设置任意一个就可以达到显示页码的功能;如果要支持 page-sizes 的更改,则需要使用 total 属性 | Number | — | — |
| pager-count | 页码按钮的数量,当总页数超过该值时会折叠 | number | 大于等于 5 且小于等于 21 的奇数 | 7 |
| current-page | 当前页数,支持 v-model 双向绑定 | number | — | 1 |
| default-current-page | 当前页数的初始值 | number | - | - |
| layout | 组件布局,子组件名用逗号分隔| String | `sizes`, `prev`, `pager`, `next`, `jumper`, `->`, `total`, `slot` | 'prev, pager, next, jumper, ->, total' |
| page-sizes | 每页显示个数选择器的选项设置 | number[] | — | [10, 20, 30, 40, 50, 100] |
| popper-class | 每页显示个数选择器的下拉框类名 | string | — | — |
@@ -228,6 +230,13 @@
| disabled | 是否禁用 | boolean | — | false |
| hide-on-single-page | 只有一页时是否隐藏 | boolean | — | - |
:::warning
我们现在会检查一些不合理的用法,如果发现分页器未显示,可以核对是否违反以下情形:
- `total``page-count` 必须传一个,不然组件无法判断总页数;优先使用 `page-count`;
- 如果传入了 `current-page` 必须监听 `current-page` 变更的事件(`onUpdate:currentPage`);否则分页切换不起作用;
- 如果传入了 `page-size`,且布局包含 `page-size` 选择器(即 `layout` 包含 `sizes`),必须监听 `page-size` 变更的事件(`onUpdate:pageSize`),否则 `page-size` 切换不起作用;
:::
### Events
| 事件名称 | 说明 | 回调参数 |
|---------|--------|---------|
@@ -236,6 +245,10 @@
| prev-click | 用户点击上一页按钮改变当前页后触发 | 当前页 |
| next-click | 用户点击下一页按钮改变当前页后触发 | 当前页 |
:::warning
以上事件不推荐使用;如果要监听 current-page 和 page-size 的改变,使用 v-model 双向绑定是个更好的选择。
:::
### Slot
| name | 说明 |
|------|--------|