mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
feat(packages/vue): add useOffsetPagination composable
This commit is contained in:
147
packages/vue/src/composables/useOffsetPagination/index.test.ts
Normal file
147
packages/vue/src/composables/useOffsetPagination/index.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { useOffsetPagination } from '.';
|
||||
|
||||
describe('useOffsetPagination', () => {
|
||||
it('initialize with default values without options', () => {
|
||||
const { currentPage, currentPageSize, totalPages, isFirstPage } = useOffsetPagination({});
|
||||
|
||||
expect(currentPage.value).toBe(1);
|
||||
expect(currentPageSize.value).toBe(10);
|
||||
expect(totalPages.value).toBe(Infinity);
|
||||
expect(isFirstPage.value).toBe(true);
|
||||
});
|
||||
|
||||
it('calculate total pages correctly', () => {
|
||||
const { totalPages } = useOffsetPagination({ total: 100, pageSize: 10 });
|
||||
|
||||
expect(totalPages.value).toBe(10);
|
||||
});
|
||||
|
||||
it('update current page correctly', () => {
|
||||
const { currentPage, next, previous, select } = useOffsetPagination({ total: 100, pageSize: 10 });
|
||||
|
||||
next();
|
||||
expect(currentPage.value).toBe(2);
|
||||
|
||||
previous();
|
||||
expect(currentPage.value).toBe(1);
|
||||
|
||||
select(5);
|
||||
expect(currentPage.value).toBe(5);
|
||||
});
|
||||
|
||||
it('handle out of bounds increments correctly', () => {
|
||||
const { currentPage, next, previous } = useOffsetPagination({ total: 10, pageSize: 5 });
|
||||
|
||||
next();
|
||||
next();
|
||||
next();
|
||||
|
||||
expect(currentPage.value).toBe(2);
|
||||
|
||||
previous();
|
||||
previous();
|
||||
previous();
|
||||
|
||||
expect(currentPage.value).toBe(1);
|
||||
});
|
||||
|
||||
it('handle page boundaries correctly', () => {
|
||||
const { currentPage, isFirstPage, isLastPage } = useOffsetPagination({ total: 20, pageSize: 10 });
|
||||
|
||||
expect(currentPage.value).toBe(1);
|
||||
expect(isFirstPage.value).toBe(true);
|
||||
expect(isLastPage.value).toBe(false);
|
||||
|
||||
currentPage.value = 2;
|
||||
|
||||
expect(currentPage.value).toBe(2);
|
||||
expect(isFirstPage.value).toBe(false);
|
||||
expect(isLastPage.value).toBe(true);
|
||||
});
|
||||
|
||||
it('call onPageChange callback', async () => {
|
||||
const onPageChange = vi.fn();
|
||||
const { currentPage, next } = useOffsetPagination({ total: 100, pageSize: 10, onPageChange });
|
||||
|
||||
next();
|
||||
await nextTick();
|
||||
|
||||
expect(onPageChange).toHaveBeenCalledTimes(1);
|
||||
expect(onPageChange).toHaveBeenCalledWith(expect.objectContaining({ currentPage: currentPage.value }));
|
||||
});
|
||||
|
||||
it('call onPageSizeChange callback', async () => {
|
||||
const onPageSizeChange = vi.fn();
|
||||
const pageSize = ref(10);
|
||||
const { currentPageSize } = useOffsetPagination({ total: 100, pageSize, onPageSizeChange });
|
||||
|
||||
pageSize.value = 20;
|
||||
await nextTick();
|
||||
|
||||
expect(onPageSizeChange).toHaveBeenCalledTimes(1);
|
||||
expect(onPageSizeChange).toHaveBeenCalledWith(expect.objectContaining({ currentPageSize: currentPageSize.value }));
|
||||
});
|
||||
|
||||
it('call onPageCountChange callback', async () => {
|
||||
const onTotalPagesChange = vi.fn();
|
||||
const total = ref(100);
|
||||
const { totalPages } = useOffsetPagination({ total, pageSize: 10, onTotalPagesChange });
|
||||
|
||||
total.value = 200;
|
||||
await nextTick();
|
||||
|
||||
expect(onTotalPagesChange).toHaveBeenCalledTimes(1);
|
||||
expect(onTotalPagesChange).toHaveBeenCalledWith(expect.objectContaining({ totalPages: totalPages.value }));
|
||||
});
|
||||
|
||||
it('handle complex reactive options', async () => {
|
||||
const total = ref(100);
|
||||
const pageSize = ref(10);
|
||||
const page = ref(1);
|
||||
|
||||
const onPageChange = vi.fn();
|
||||
const onPageSizeChange = vi.fn();
|
||||
const onTotalPagesChange = vi.fn();
|
||||
|
||||
const { currentPage, currentPageSize, totalPages } = useOffsetPagination({
|
||||
total,
|
||||
pageSize,
|
||||
page,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onTotalPagesChange,
|
||||
});
|
||||
|
||||
// Initial values
|
||||
expect(currentPage.value).toBe(1);
|
||||
expect(currentPageSize.value).toBe(10);
|
||||
expect(totalPages.value).toBe(10);
|
||||
expect(onPageChange).toHaveBeenCalledTimes(0);
|
||||
expect(onPageSizeChange).toHaveBeenCalledTimes(0);
|
||||
expect(onTotalPagesChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
total.value = 300;
|
||||
pageSize.value = 15;
|
||||
page.value = 2;
|
||||
await nextTick();
|
||||
|
||||
// Valid values after changes
|
||||
expect(currentPage.value).toBe(2);
|
||||
expect(currentPageSize.value).toBe(15);
|
||||
expect(totalPages.value).toBe(20);
|
||||
expect(onPageChange).toHaveBeenCalledTimes(1);
|
||||
expect(onPageSizeChange).toHaveBeenCalledTimes(1);
|
||||
expect(onTotalPagesChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
page.value = 21;
|
||||
await nextTick();
|
||||
|
||||
// Invalid values after changes
|
||||
expect(currentPage.value).toBe(20);
|
||||
expect(onPageChange).toHaveBeenCalledTimes(2);
|
||||
expect(onPageSizeChange).toHaveBeenCalledTimes(1);
|
||||
expect(onTotalPagesChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
124
packages/vue/src/composables/useOffsetPagination/index.ts
Normal file
124
packages/vue/src/composables/useOffsetPagination/index.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { VoidFunction } from '@robonen/stdlib';
|
||||
import { computed, reactive, toValue, watch, type ComputedRef, type MaybeRef, type MaybeRefOrGetter, type UnwrapNestedRefs, type WritableComputedRef } from 'vue';
|
||||
import { useClamp } from '../useClamp';
|
||||
|
||||
// TODO: sync returned refs with passed refs
|
||||
|
||||
export interface UseOffsetPaginationOptions {
|
||||
total?: MaybeRefOrGetter<number>;
|
||||
pageSize?: MaybeRef<number>;
|
||||
page?: MaybeRef<number>;
|
||||
onPageChange?: (returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown;
|
||||
onPageSizeChange?: (returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown;
|
||||
onTotalPagesChange?: (returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown;
|
||||
}
|
||||
|
||||
export interface UseOffsetPaginationReturn {
|
||||
currentPage: WritableComputedRef<number>;
|
||||
currentPageSize: WritableComputedRef<number>;
|
||||
totalPages: ComputedRef<number>;
|
||||
isFirstPage: ComputedRef<boolean>;
|
||||
isLastPage: ComputedRef<boolean>;
|
||||
next: VoidFunction;
|
||||
previous: VoidFunction;
|
||||
select: (page: number) => void;
|
||||
}
|
||||
|
||||
export type UseOffsetPaginationInfinityReturn = Omit<UseOffsetPaginationReturn, 'isLastPage'>;
|
||||
|
||||
/**
|
||||
* @name useOffsetPagination
|
||||
* @category Utilities
|
||||
* @description A composable function that provides pagination functionality for offset based pagination
|
||||
*
|
||||
* @param {UseOffsetPaginationOptions} options The options for the pagination
|
||||
* @param {MaybeRefOrGetter<number>} options.total The total number of items
|
||||
* @param {MaybeRef<number>} options.pageSize The number of items per page
|
||||
* @param {MaybeRef<number>} options.page The current page
|
||||
* @param {(returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown} options.onPageChange A callback that is called when the page changes
|
||||
* @param {(returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown} options.onPageSizeChange A callback that is called when the page size changes
|
||||
* @param {(returnValue: UnwrapNestedRefs<UseOffsetPaginationReturn>) => unknown} options.onTotalPagesChange A callback that is called when the total number of pages changes
|
||||
* @returns {UseOffsetPaginationReturn} The pagination object
|
||||
*
|
||||
* @example
|
||||
* const {
|
||||
* currentPage,
|
||||
* currentPageSize,
|
||||
* totalPages,
|
||||
* isFirstPage,
|
||||
* isLastPage,
|
||||
* next,
|
||||
* previous,
|
||||
* select,
|
||||
* } = useOffsetPagination({ total: 100, pageSize: 10, page: 1 });
|
||||
*
|
||||
* @example
|
||||
* const {
|
||||
* currentPage,
|
||||
* } = useOffsetPagination({
|
||||
* total: 100,
|
||||
* pageSize: 10,
|
||||
* page: 1,
|
||||
* onPageChange: ({ currentPage }) => console.log(currentPage),
|
||||
* onPageSizeChange: ({ currentPageSize }) => console.log(currentPageSize),
|
||||
* onTotalPagesChange: ({ totalPages }) => console.log(totalPages),
|
||||
* });
|
||||
*/
|
||||
export function useOffsetPagination(options: Omit<UseOffsetPaginationOptions, 'total'>): UseOffsetPaginationInfinityReturn;
|
||||
export function useOffsetPagination(options: UseOffsetPaginationOptions): UseOffsetPaginationReturn;
|
||||
export function useOffsetPagination(options: UseOffsetPaginationOptions): UseOffsetPaginationReturn {
|
||||
const {
|
||||
total = Number.POSITIVE_INFINITY,
|
||||
pageSize = 10,
|
||||
page = 1,
|
||||
} = options;
|
||||
|
||||
const currentPageSize = useClamp(pageSize, 1, Number.POSITIVE_INFINITY);
|
||||
|
||||
const totalPages = computed(() => Math.max(
|
||||
1,
|
||||
Math.ceil(toValue(total) / toValue(currentPageSize))
|
||||
));
|
||||
|
||||
const currentPage = useClamp(page, 1, totalPages);
|
||||
|
||||
const isFirstPage = computed(() => currentPage.value === 1);
|
||||
const isLastPage = computed(() => currentPage.value === totalPages.value);
|
||||
|
||||
const next = () => currentPage.value++;
|
||||
const previous = () => currentPage.value--;
|
||||
const select = (page: number) => currentPage.value = page;
|
||||
|
||||
const returnValue = {
|
||||
currentPage,
|
||||
currentPageSize,
|
||||
totalPages,
|
||||
isFirstPage,
|
||||
isLastPage,
|
||||
next,
|
||||
previous,
|
||||
select,
|
||||
};
|
||||
|
||||
// NOTE: Don't forget to await nextTick() after calling next() or previous() to ensure the callback is called
|
||||
|
||||
if (options.onPageChange) {
|
||||
watch(currentPage, () => {
|
||||
options.onPageChange!(reactive(returnValue));
|
||||
});
|
||||
}
|
||||
|
||||
if (options.onPageSizeChange) {
|
||||
watch(currentPageSize, () => {
|
||||
options.onPageSizeChange!(reactive(returnValue));
|
||||
});
|
||||
}
|
||||
|
||||
if (options.onTotalPagesChange) {
|
||||
watch(totalPages, () => {
|
||||
options.onTotalPagesChange!(reactive(returnValue));
|
||||
});
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
Reference in New Issue
Block a user