1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 02:44:45 +00:00

feat(packages/vue): add useOffsetPagination composable

This commit is contained in:
2024-10-21 06:48:47 +07:00
parent ae6154c4b6
commit a5ba8ab13e
2 changed files with 271 additions and 0 deletions

View 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);
});
});

View 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;
}