Creating custom components for the grid.
Create custom components to extend the ActiveGrid's functionality. Build custom cells, headers, editors, filters, and more that integrate seamlessly with the grid.
type StatusCellProps = {
status: string;
};
const StatusCell: React.FC<StatusCellProps> = ({ status }) => {
const variant =
status === 'active' ? 'default' :
status === 'pending' ? 'secondary' :
'outline';
return <Badge variant={variant}>{status}</Badge>;
};
// Use in column
{
accessorKey: 'status',
header: 'Status',
cell: ({ getValue }) => <StatusCell status={getValue() as string} />,
}type UserCellProps = {
user: User;
onEdit: (user: User) => void;
};
const UserCell: React.FC<UserCellProps> = ({ user, onEdit }) => {
return (
<div className="flex items-center gap-2">
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{user.name}</div>
<div className="text-sm text-muted-foreground">{user.email}</div>
</div>
<Button size="sm" variant="ghost" onClick={() => onEdit(user)}>
<Pencil className="h-3 w-3" />
</Button>
</div>
);
};
// Use in column
{
accessorKey: 'name',
header: 'User',
cell: ({ row }) => (
<UserCell user={row.original} onEdit={handleEdit} />
),
}import { Column } from '@tanstack/react-table';
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
type SortableHeaderProps<TData> = {
column: Column<TData>;
title: string;
};
function SortableHeader<TData>({ column, title }: SortableHeaderProps<TData>) {
const sorted = column.getIsSorted();
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting()}
className="h-auto p-0 hover:bg-transparent"
>
{title}
{sorted === 'asc' && <ArrowUp className="ml-2 h-4 w-4" />}
{sorted === 'desc' && <ArrowDown className="ml-2 h-4 w-4" />}
{!sorted && <ArrowUpDown className="ml-2 h-4 w-4 opacity-50" />}
</Button>
);
}
// Use in column
{
accessorKey: 'name',
header: ({ column }) => <SortableHeader column={column} title="Name" />,
}type FilterableHeaderProps<TData> = {
column: Column<TData>;
title: string;
};
function FilterableHeader<TData>({ column, title }: FilterableHeaderProps<TData>) {
const [value, setValue] = useState('');
return (
<div className="space-y-2">
<div className="font-medium">{title}</div>
<Input
placeholder={`Filter ${title}...`}
value={value}
onChange={(e) => {
setValue(e.target.value);
column.setFilterValue(e.target.value);
}}
className="h-8"
/>
</div>
);
}
// Use in column
{
accessorKey: 'name',
header: ({ column }) => <FilterableHeader column={column} title="Name" />,
}import { useState, useEffect, useRef } from 'react';
type RichTextEditorProps = {
value: string;
onChange: (value: string) => void;
onBlur: () => void;
autoFocus?: boolean;
};
const RichTextEditor: React.FC<RichTextEditorProps> = ({
value,
onChange,
onBlur,
autoFocus,
}) => {
const [localValue, setLocalValue] = useState(value);
const editorRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (autoFocus && editorRef.current) {
editorRef.current.focus();
}
}, [autoFocus]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setLocalValue(e.target.value);
onChange(e.target.value);
};
return (
<Textarea
ref={editorRef}
value={localValue}
onChange={handleChange}
onBlur={onBlur}
className="min-h-[100px]"
/>
);
};
// Use in column
{
accessorKey: 'description',
header: 'Description',
meta: {
editable: true,
customEditor: RichTextEditor,
},
}import { DateRange } from 'react-day-picker';
type DateRangeEditorProps = {
value: { from: Date; to: Date };
onChange: (value: { from: Date; to: Date }) => void;
onBlur: () => void;
};
const DateRangeEditor: React.FC<DateRangeEditorProps> = ({
value,
onChange,
onBlur,
}) => {
const [range, setRange] = useState<DateRange>({
from: value.from,
to: value.to,
});
const handleSelect = (newRange: DateRange | undefined) => {
if (newRange?.from && newRange?.to) {
setRange(newRange);
onChange({ from: newRange.from, to: newRange.to });
}
};
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
{format(range.from, 'PP')} - {format(range.to, 'PP')}
</Button>
</PopoverTrigger>
<PopoverContent>
<Calendar
mode="range"
selected={range}
onSelect={handleSelect}
/>
</PopoverContent>
</Popover>
);
};import { Column } from '@tanstack/react-table';
type MultiSelectFilterProps<TData> = {
column: Column<TData>;
options: Array<{ label: string; value: string }>;
};
function MultiSelectFilter<TData>({
column,
options,
}: MultiSelectFilterProps<TData>) {
const [selected, setSelected] = useState<string[]>([]);
const handleToggle = (value: string) => {
const newSelected = selected.includes(value)
? selected.filter(v => v !== value)
: [...selected, value];
setSelected(newSelected);
column.setFilterValue(newSelected.length > 0 ? newSelected : undefined);
};
return (
<div className="space-y-1">
{options.map(option => (
<label key={option.value} className="flex items-center gap-2">
<Checkbox
checked={selected.includes(option.value)}
onCheckedChange={() => handleToggle(option.value)}
/>
{option.label}
</label>
))}
</div>
);
}
// Use in column
{
accessorKey: 'tags',
header: 'Tags',
filterFn: (row, columnId, filterValue: string[]) => {
const rowTags = row.getValue(columnId) as string[];
return filterValue.some(tag => rowTags.includes(tag));
},
}import { Table } from '@tanstack/react-table';
type AdvancedToolbarProps<TData> = {
table: Table<TData>;
};
function AdvancedToolbar<TData>({ table }: AdvancedToolbarProps<TData>) {
const selectedCount = table.getSelectedRowModel().rows.length;
const filteredCount = table.getFilteredRowModel().rows.length;
return (
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-4">
<Input
placeholder="Search..."
value={table.getState().globalFilter ?? ''}
onChange={(e) => table.setGlobalFilter(e.target.value)}
className="w-64"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Filter className="mr-2 h-4 w-4" />
Filters
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{/* Custom filters */}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-2">
{selectedCount > 0 && (
<div className="flex items-center gap-2">
<Badge>{selectedCount} selected</Badge>
<Button
variant="outline"
size="sm"
onClick={() => table.resetRowSelection()}
>
Clear
</Button>
</div>
)}
<div className="text-sm text-muted-foreground">
{filteredCount} rows
</div>
</div>
</div>
);
}
// Use in ActiveGrid
<ActiveGrid
toolbar={{
hidden: true, // Hide default toolbar
}}
{...}
/>
<AdvancedToolbar table={table} />const CustomEmptyState = () => {
return (
<div className="flex flex-col items-center justify-center p-12">
<div className="rounded-full bg-muted p-4 mb-4">
<Inbox className="h-12 w-12 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2">No data found</h3>
<p className="text-muted-foreground mb-4">
Get started by adding your first record
</p>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Record
</Button>
</div>
);
};
// Use in ActiveGrid
<ActiveGrid
settings={{
noDataOverlay: <CustomEmptyState />,
}}
{...}
/>type OrderDetailPanelProps = {
order: Order;
};
const OrderDetailPanel: React.FC<OrderDetailPanelProps> = ({ order }) => {
return (
<div className="p-6 bg-muted/20 border-t">
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="font-semibold mb-2">Order Details</h4>
<dl className="space-y-1">
<div className="flex justify-between">
<dt className="text-muted-foreground">Order ID:</dt>
<dd className="font-mono">{order.id}</dd>
</div>
<div className="flex justify-between">
<dt className="text-muted-foreground">Date:</dt>
<dd>{format(order.createdAt, 'PPP')}</dd>
</div>
<div className="flex justify-between">
<dt className="text-muted-foreground">Status:</dt>
<dd><Badge>{order.status}</Badge></dd>
</div>
</dl>
</div>
<div>
<h4 className="font-semibold mb-2">Customer</h4>
<p>{order.customer.name}</p>
<p className="text-sm text-muted-foreground">
{order.customer.email}
</p>
</div>
</div>
<div className="mt-4">
<h4 className="font-semibold mb-2">Items</h4>
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Qty</TableHead>
<TableHead>Price</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{order.items.map(item => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell>{item.quantity}</TableCell>
<TableCell>${item.price.toFixed(2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
// Use in ActiveGrid
<ActiveGrid
enableExpanding={true}
renderDetailPanel={({ row }) => (
<OrderDetailPanel order={row.original} />
)}
{...}
/>import { useState, useMemo } from 'react';
import {
ActiveGrid,
GridColumnDef,
getExpanderColumn,
} from '@workspace/active-grid';
type Product = {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
description: string;
};
function ProductsTable() {
const [products, setProducts] = useState<Product[]>([]);
const columns = useMemo<GridColumnDef<Product>[]>(() => [
getExpanderColumn<Product>(),
{
accessorKey: 'name',
header: ({ column }) => (
<SortableHeader column={column} title="Product" />
),
cell: ({ row }) => (
<ProductCell product={row.original} />
),
},
{
accessorKey: 'price',
header: 'Price',
cell: ({ getValue }) => (
<PriceCell amount={getValue() as number} />
),
},
{
accessorKey: 'category',
header: ({ column }) => (
<FilterableHeader column={column} title="Category" />
),
},
{
accessorKey: 'inStock',
header: 'In Stock',
cell: ({ getValue }) => (
<StockCell inStock={getValue() as boolean} />
),
},
], []);
return (
<ActiveGrid
columns={columns}
data={products}
enableExpanding={true}
renderDetailPanel={({ row }) => (
<ProductDetailPanel product={row.original} />
)}
settings={{
noDataOverlay: <CustomEmptyState />,
}}
/>
);
}