Data Table
Powerful data table built using TanStack Table.
Experimental
Things may change quickly
| Name | Status | Created At | |
|---|---|---|---|
| Alice | alice@gmail.com | Confirmed | 2024-01-21 |
| Bob | bob@gmail.com | Is Pending | 2024-01-30 |
| Cisne | cisne@gmail.com | Is Pending | 2024-01-30 |
| Charlie | charlie@gmail.com | Success | 2024-02-01 |
"use client"
import * as React from "react";
import { MailIcon, PlusIcon, RotateCcw, SearchIcon } from "lucide-react";
import { Badge, Button, InputGroup, InputGroupAddon, InputGroupInput, Tooltip, TooltipContent, TooltipTrigger } from "uilab-core";Introduction
Every data table is unique and it depends on your use case and business logic. It's hard to have one data table that fits every single data display. Some needs to be simple and some needs to have complex filters and paginations (e.g. fintech stuff).
We have made 2 types of data tables using Tanstack Table which is a headless UI. Capability of this package is endless so you can change certain code to fit your use case.
Tanstack Router's type-safe routing feature is super useful for filters and pagination, so we will be using it for demonstrations below.
Installation
npx shadcn add @uilab-core/data-tableBasic Table
Pretty basic and simple table. Made this for smaller data that does not need pagination or any other feature.
| Key | Value | Environment |
|---|---|---|
API_KEY | 1234567890 | Production |
DB_PASSWORD | password123 | Staging |
SECRET_KEY | secretkey123 | Development |
"use client"
import { BasicDataTable } from "@/components/data-table";
import { CellContext, ColumnDef } from "@tanstack/react-table";
import { Badge } from "uilab-core";File structure
Start by creating the following file structure:
products
├── index.tsx
└── -components
└── products-table
├── columns.tsx
└── index.tsxColumn definitions
export default [
{
accessorKey: "name",
header: "Name"
},
{
accessorKey: "email",
header: "Email"
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }: CellContext<DataItem, unknown>) => (
<Badge variant="outline">{row.getValue('status')}</Badge>
)
},
{
accessorKey: "createdAt",
header: "Created At"
}
] as Array<ColumnDef<DataItem>>Render the table
Prepare our table component to render on page
import columns from "./columns"
import BasicDataTable from "@/components/data-table"
export function Table() {
const { data, isFetching } = useQuery(dataQuery())
return (
<BasicDataTable
columns={columns}
data={data}
isFetching={isFetching}
/>
);
}Advanced Table
| Name | Status | Created At | |
|---|---|---|---|
| Alice | alice@gmail.com | Confirmed | 2024-01-21 |
| Bob | bob@gmail.com | Is Pending | 2024-01-30 |
| Cisne | cisne@gmail.com | Is Pending | 2024-01-30 |
| Charlie | charlie@gmail.com | Success | 2024-02-01 |
"use client"
import * as React from "react";
import { MailIcon, PlusIcon, RotateCcw, SearchIcon } from "lucide-react";
import { Badge, Button, InputGroup, InputGroupAddon, InputGroupInput, Tooltip, TooltipContent, TooltipTrigger } from "uilab-core";File structure
Start by creating the following file structure:
products
├── index.tsx
└── -components
├── index.tsx
└── table
├── columns.tsx
├── filter.tsx
└── index.tsxValidate search params
Page needs to validate if given search params is valid or not. We are using zod library for validation.
import { createFileRoute } from "@tanstack/react-router"
import * as z from "zod"
import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from "@/lib/constants"
import { Table } from "./-components/table"
export const productsSearchSchema = z.object({
pageIndex: z.number().catch(DEFAULT_PAGE_INDEX).optional(),
pageSize: z.number().catch(DEFAULT_PAGE_SIZE).optional(),
})
export const Route = createFileRoute("/products/")({
component: ProductsPage,
validateSearch: productsSearchSchema.parse,
})
function ProductsPage() {
return <Table />
}Filter component
Reminder
Filter form components should be controlled components, so when sharing the url, the values are instantly filled into the form components. Ease of use.
import { FormProvider, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { productsSearchSchema } from "../../"
export default function Filter() {
const { filters, setFilters, resetFilters } = useFilters(PATH)
const form = useForm({
resolver: zodResolver(productsSearchSchema),
values: filters,
})
const handleSubmit = form.handleSubmit((items: SearchSchema) => {
setFilters(items)
})
const handleReset = () => {
resetFilters().then(() => {
form.reset()
form.clearErrors()
})
}
return (
<FormProvider {...form}>
<form
className="flex flex-wrap items-end gap-2 rounded-lg border border-dashed p-2"
onSubmit={handleSubmit}
>
<InputGroup>
<InputGroupAddon>
<MailIcon />
</InputGroupAddon>
<InputGroupInput
placeholder="Filter emails..."
value={emailFilter}
onChange={(e) => setEmailFilter(e.target.value)}
/>
</InputGroup>
<Button variant="outline" size="icon" onClick={handleReset}>
<RotateCcw />
</Button>
<Button type="submit">
<SearchIcon />
{t("search")}
</Button>
</form>
</FormProvider>
)
}Column definition
export default [
{
accessorKey: "name",
header: "Name"
},
{
accessorKey: "email",
header: "Email"
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }: CellContext<DataItem, unknown>) => (
<Badge variant="outline">{row.getValue('status')}</Badge>
)
},
{
accessorKey: "createdAt",
header: "Created At"
}
] as Array<ColumnDef<DataItem>>Render the table
import { AdvancedDataTable } from "@/components/data-table"
import { useFilters } from "@/hooks/use-filter"
import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from "@/lib/constants"
import Filter from "./filter"
import columns from "./columns"
export function Table() {
// This filter will be using `/products` page's validated search params.
const { filters, setFilters } = useFilters("/products/")
const paginationState = {
pageIndex: filters.pageIndex ?? DEFAULT_PAGE_INDEX,
pageSize: filters.pageSize ?? DEFAULT_PAGE_SIZE,
}
const { data, isFetching, refetch } = useQuery(
dataQuery({
...filters,
page: paginationState.pageIndex.toString(),
size: paginationState.pageSize.toString(),
}),
)
return (
<AdvancedDataTable
columns={COLUMNS}
data={data}
refetch={refetch}
isFetching={isFetching}
leftHeader={<Filter />}
// For example: You can add sheet components with trigger
rightHeader={<Button><PlusIcon />Add</Button>}
pagination={paginationState}
/>
);
}