Last active
November 22, 2024 13:25
-
-
Save nicostombros/fc9a16a67fc47a536cf84736d0b8cfd8 to your computer and use it in GitHub Desktop.
Shadcn UI implementation of DataTable and Pagination components working together
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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