Skip to content

Instantly share code, notes, and snippets.

@nicostombros
Last active November 12, 2024 06:10
Show Gist options
  • Save nicostombros/fc9a16a67fc47a536cf84736d0b8cfd8 to your computer and use it in GitHub Desktop.
Save nicostombros/fc9a16a67fc47a536cf84736d0b8cfd8 to your computer and use it in GitHub Desktop.
Shadcn UI implementation of DataTable and Pagination components working together
// same as your regular shadcn datatable implementation (which uses tanstack table)
// note that the shadcn datatable uses shadcn table for the ui. see https://ui.shadcn.com/docs/components/data-table
import {
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import * as React from "react"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { ColumnDef } from "@tanstack/react-table"
import Paginator from "./paginator"
export interface DataTableProps {
data: any[];
columns: ColumnDef<any, any>[];
}
export default function DataTable({
data,
columns,
}: DataTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
enableRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
})
return (
<div className="w-full">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 flex-auto"
>
<div className="flex justify-center">
No results
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{
table.getFilteredSelectedRowModel().rows.length ? (
`${table.getFilteredSelectedRowModel().rows.length} of{" "}`
) : null
}
{table.getFilteredRowModel().rows.length} row(s)
</div>
<div className="flex justify-end">
<Paginator
currentPage={table.getState().pagination.pageIndex + 1}
totalPages={table.getPageCount()}
onPageChange={(pageNumber) => table.setPageIndex(pageNumber - 1)}
showPreviousNext
/>
</div>
</div>
</div>
)
}
// this implementation can be a bit jumpy for larger tables, but should be good for most and easily adaptable if not
// this file is where your logic for how when ellipses are shown and other fiddly bits
import { PaginationEllipsis, PaginationItem, PaginationLink } from "@/components/ui/pagination";
export const generatePaginationLinks = (
currentPage: number,
totalPages: number,
onPageChange: (page: number) => void
) => {
const pages: JSX.Element[] = [];
if (totalPages <= 6) {
for (let i = 1; i <= totalPages; i++) {
pages.push(
<PaginationItem key={i}>
<PaginationLink
onClick={() => onPageChange(i)}
isActive={i === currentPage}
>
{i}
</PaginationLink>
</PaginationItem>
);
}
} else {
for (let i = 1; i <= 2; i++) {
pages.push(
<PaginationItem key={i}>
<PaginationLink
onClick={() => onPageChange(i)}
isActive={i === currentPage}
>
{i}
</PaginationLink>
</PaginationItem>
);
}
if (2 < currentPage && currentPage < totalPages - 1) {
pages.push(<PaginationEllipsis />)
pages.push(
<PaginationItem key={currentPage}>
<PaginationLink
onClick={() => onPageChange(currentPage)}
isActive={true}
>
{currentPage}
</PaginationLink>
</PaginationItem>
);
}
pages.push(<PaginationEllipsis />)
for (let i = totalPages - 1; i <= totalPages; i++) {
pages.push(
<PaginationItem key={i}>
<PaginationLink
onClick={() => onPageChange(i)}
isActive={i === currentPage}
>
{i}
</PaginationLink>
</PaginationItem>
);
}
}
return pages;
};
// same as the shadcn pagination you're given, so use your own if you wish
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("hover:cursor-pointer", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean;
disabled?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
disabled = false,
...props
}: PaginationLinkProps) => (
<PaginationItem>
<a
aria-current={isActive ? "page" : undefined}
role="link"
aria-disabled={disabled}
className={cn(
buttonVariants({
variant: isActive ? "default" : "ghost",
size,
}),
disabled && "cursor-not-allowed pointer-events-none",
className
)}
{...props}
/>
</PaginationItem>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}
// this is where you'd implement some pagination logic like whether a next page is available, which can then be imported to the DataTable
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { generatePaginationLinks } from "./generate-pages";
type PaginatorProps = {
currentPage: number;
totalPages: number;
onPageChange: (pageNumber: number) => void;
showPreviousNext: boolean;
}
export default function Paginator({
currentPage,
totalPages,
onPageChange,
showPreviousNext,
}: PaginatorProps) {
return (
<Pagination>
<PaginationContent>
{showPreviousNext && totalPages ? (
<PaginationItem>
<PaginationPrevious
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage - 1 < 1}
/>
</PaginationItem>
) : null}
{generatePaginationLinks(currentPage, totalPages, onPageChange)}
{showPreviousNext && totalPages ? (
<PaginationItem>
<PaginationNext
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage > totalPages - 1}
/>
</PaginationItem>
): null}
</PaginationContent>
</Pagination>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment