#Custom Fields
This example passes custom Fields to useSearchPagination(fields), uses p and size as URL keys, and limits page size to 6, 12, and 24.
In custom Fields scenarios, the codec and selector should consume the same local pageSizeOptions. useSearchPagination.pageSizeOptions only belongs to the default pagination model and should not be used here.
Result 1#1
Result 2#2
Result 3#3
Result 4#4
Result 5#5
Result 6#6
1-6 of 58
Page size
(empty)p: 1size: 6
import { field } from '@guanriyue/decurl/codec';
import {
elementOf,
min,
pipe,
shape,
toNumber,
trim,
where,
} from '@guanriyue/decurl/decode';
import type { SearchPaginationFields } from '@guanriyue/decurl/pagination';
import { useSearchPagination } from '@guanriyue/decurl/pagination';
import { useEffect } from 'react';
import { useLocation } from 'react-router';
import { Badge } from '@/components/ui/badge';
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
const pageSizeOptions = [6, 12, 24];
const fields = {
page: field({
name: 'p',
decode: pipe(
trim,
shape.integer,
toNumber,
where(Number.isSafeInteger),
min(1),
),
defaultValue: 1,
}),
pageSize: field({
name: 'size',
decode: pipe(trim, shape.integer, toNumber, elementOf(pageSizeOptions)),
defaultValue: 6,
}),
} satisfies SearchPaginationFields;
const items = Array.from({ length: 58 }, (_, index) => {
const id = index + 1;
return {
id,
label: `Result ${id}`,
};
});
const toSearchText = (search: string): string => {
return search.length > 0 ? search : '(empty)';
};
const getVisiblePages = (page: number, pageCount: number): number[] => {
const start = Math.max(1, page - 1);
const end = Math.min(pageCount, page + 1);
return Array.from({ length: end - start + 1 }, (_, index) => start + index);
};
const CustomPaginationDemo = () => {
const location = useLocation();
const pagination = useSearchPagination(fields);
const pageCount = Math.max(1, Math.ceil(items.length / pagination.pageSize));
const page = Math.min(pagination.page, pageCount);
const startIndex = (page - 1) * pagination.pageSize;
const currentItems = items.slice(
startIndex,
startIndex + pagination.pageSize,
);
const visiblePages = getVisiblePages(page, pageCount);
const currentHref = `${location.pathname}${location.search}`;
useEffect(() => {
pagination.preventOverflow(items.length);
}, [pagination.preventOverflow]);
const updatePage = (nextPage: number) => {
pagination.setPage(Math.min(Math.max(nextPage, 1), pageCount));
};
return (
<section className="mx-auto w-full max-w-6xl space-y-5 p-4">
<div className="grid min-h-48 content-start gap-2 sm:grid-cols-2 lg:grid-cols-4">
{currentItems.map((item) => (
<div
key={item.id}
className="flex h-14 items-center justify-between rounded-md bg-muted/50 px-3"
>
<span className="text-sm font-medium">{item.label}</span>
<Badge variant="outline">#{item.id}</Badge>
</div>
))}
</div>
<div className="grid items-center gap-4 border-t pt-4 md:grid-cols-[1fr_auto_1fr]">
<p className="text-sm text-muted-foreground">
{startIndex + 1}-{startIndex + currentItems.length} of {items.length}
</p>
<Pagination className="w-auto">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={currentHref}
aria-disabled={page <= 1}
className={cn(page <= 1 && 'pointer-events-none opacity-50')}
onClick={(event) => {
event.preventDefault();
updatePage(page - 1);
}}
/>
</PaginationItem>
{visiblePages[0] > 1 && (
<>
<PaginationItem>
<PaginationLink
href={currentHref}
onClick={(event) => {
event.preventDefault();
updatePage(1);
}}
>
1
</PaginationLink>
</PaginationItem>
{visiblePages[0] > 2 && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
</>
)}
{visiblePages.map((visiblePage) => (
<PaginationItem key={visiblePage}>
<PaginationLink
href={currentHref}
isActive={visiblePage === page}
onClick={(event) => {
event.preventDefault();
updatePage(visiblePage);
}}
>
{visiblePage}
</PaginationLink>
</PaginationItem>
))}
{visiblePages[visiblePages.length - 1] < pageCount && (
<>
{visiblePages[visiblePages.length - 1] < pageCount - 1 && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
<PaginationItem>
<PaginationLink
href={currentHref}
onClick={(event) => {
event.preventDefault();
updatePage(pageCount);
}}
>
{pageCount}
</PaginationLink>
</PaginationItem>
</>
)}
<PaginationItem>
<PaginationNext
href={currentHref}
aria-disabled={page >= pageCount}
className={cn(
page >= pageCount && 'pointer-events-none opacity-50',
)}
onClick={(event) => {
event.preventDefault();
updatePage(page + 1);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<div className="flex items-center gap-2 md:justify-end">
<span className="text-sm text-muted-foreground">Page size</span>
<Select
value={String(pagination.pageSize)}
onValueChange={(value) => {
pagination.setPageSize(Number(value));
}}
>
<SelectTrigger size="sm" className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
{pageSizeOptions.map((pageSize) => (
<SelectItem key={pageSize} value={String(pageSize)}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-3 rounded-md bg-muted/40 p-4 text-sm">
<code className="block break-all font-mono text-xs">
{toSearchText(location.search)}
</code>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">p: {pagination.page}</Badge>
<Badge variant="secondary">size: {pagination.pageSize}</Badge>
</div>
</div>
</section>
);
};
export default CustomPaginationDemo;