UNPKG

tanstack-shadcn-table

Version:

A powerful, feature-rich React table component built on top of TanStack Table v8 with shadcn/ui styling. Optimized bundle size with 55% reduction through peer dependencies.

1,152 lines (948 loc) 26.9 kB
# Tanstack Shadcn Table A powerful, feature-rich React table component built on top of TanStack Table v8 with shadcn/ui styling. This library provides a complete data table solution with advanced features like filtering, sorting, pagination, column reordering, row selection, and lazy loading. ## 🚀 Features - **🎨 Beautiful UI**: Built with shadcn/ui components and Tailwind CSS - **📊 Advanced Filtering**: Text, range, select, boolean, and custom filters - **🔄 Sorting**: Multi-column sorting with fuzzy search support - **📄 Pagination**: Flexible pagination with customizable layouts - **🔀 Column Reordering**: Drag and drop column reordering - **📏 Column Resizing**: Interactive column width adjustment with drag handles - **✅ Row Selection**: Single and multi-row selection - **🔍 Global Search**: Fuzzy search across all columns - **⚡ Lazy Loading**: Server-side data loading support - **👁️ Column Visibility**: Show/hide columns dynamically - **🔒 Security**: Built-in XSS protection and input sanitization - **📱 Responsive**: Mobile-friendly design - **🎯 TypeScript**: Full TypeScript support - **🧩 Customizable**: Highly customizable components and styling - **⚡ Optimized Bundle**: 55% smaller bundle size with peer dependencies - **🌍 i18n Support**: Built-in internationalization for 5 languages ## 📦 Installation ```bash npm install tanstack-shadcn-table ``` ### 📎 Styles This library ships its own compiled CSS so Tailwind utilities used internally (e.g., `h-9`) always work, even if your app doesn't use Tailwind or purges different class sets. Import the CSS once in your app entry: ```ts import 'tanstack-shadcn-table/dist/styles.css'; ``` ### 📊 Bundle Size - **Library Size**: ~96KB (gzipped) - **With Dependencies**: ~150KB (when peer dependencies are shared) - **Optimization**: 55% smaller than traditional bundling approach ### Peer Dependencies This library is designed to be lightweight and efficient. The following dependencies are required as peer dependencies to avoid bundle duplication: ```bash npm install @radix-ui/react-checkbox @radix-ui/react-dropdown-menu @radix-ui/react-select @radix-ui/react-slot @tanstack/react-table @tanstack/match-sorter-utils class-variance-authority clsx lucide-react react react-dom tailwind-merge ``` **Why Peer Dependencies?** - **Bundle Size Optimization**: Reduces final bundle size by ~55% - **Version Flexibility**: Allows you to use your preferred versions - **Tree Shaking**: Better optimization when dependencies are external - **Conflict Prevention**: Avoids version conflicts in your application ## 🎯 Quick Start ```tsx import { DataTable, ColumnDef } from "tanstack-shadcn-table"; type Person = { firstName: string; lastName: string; age: number; email: string; }; const columns: ColumnDef<Person>[] = [ { accessorKey: "firstName", header: "First Name", filter: { type: "text", field: "firstName", placeholder: "Search first name...", }, }, { accessorKey: "lastName", header: "Last Name", }, { accessorKey: "age", header: "Age", filter: { type: "range", field: "age", }, }, { accessorKey: "email", header: "Email", }, ]; const data: Person[] = [ { firstName: "John", lastName: "Doe", age: 30, email: "john@example.com" }, { firstName: "Jane", lastName: "Smith", age: 25, email: "jane@example.com" }, ]; function App() { return ( <DataTable tableOptions={{ data, columns, pagination: { pageSize: 10, totalRecords: data.length, }, }} /> ); } ``` ## 📚 API Reference ### DataTable Props | Prop | Type | Description | | ---------------------- | --------------------- | ---------------------------------------------- | | `tableOptions` | `TableOptions<TData>` | Main configuration object for the table | | `className` | `string` | Additional CSS classes for the table container | | `TableComponent` | `React.ElementType` | Custom table component (default: shadcn Table) | | `TableHeaderComponent` | `React.ElementType` | Custom table header component | | `TableRowComponent` | `React.ElementType` | Custom table row component | | `TableCellComponent` | `React.ElementType` | Custom table cell component | | `TableHeadComponent` | `React.ElementType` | Custom table head component | | `TableBodyComponent` | `React.ElementType` | Custom table body component | | `TableFooterComponent` | `React.ElementType` | Custom table footer component | ### TableOptions ```tsx type TableOptions<T> = { data: T[]; columns: ColumnDef<T>[]; globalFilter?: GlobalFilterType; filterRowClassName?: string; rowClassName?: string; colClassName?: string; filter?: boolean; reorderable?: boolean; pagination?: PaginationOptions; // Column sizing options enableColumnResizing?: boolean; columnResizeMode?: "onChange" | "onEnd"; columnResizeDirection?: "ltr" | "rtl"; // State management (optional - for controlled components) columnFilters?: ColumnFiltersState; onColumnFiltersChange?: (filters: ColumnFiltersState) => void; sorting?: SortingState[]; onSortingChange?: (sorting: SortingState[]) => void; paginationState?: PaginationState; onPaginationChange?: (pagination: PaginationState) => void; columnVisibility?: Record<string, boolean>; onColumnVisibilityChange?: (visibility: Record<string, boolean>) => void; columnOrder?: string[]; onColumnOrderChange?: (order: string[]) => void; rowSelection?: Record<string, boolean>; onRowSelectionChange?: (selection: Record<string, boolean>) => void; columnSizing?: ColumnSizingState; onColumnSizingChange?: (sizing: ColumnSizingState) => void; defaultColumn?: { size?: number; minSize?: number; maxSize?: number; }; // Lazy loading lazy?: boolean; onLazyLoad?: (event: LazyLoadEvent) => void; }; ``` ### ColumnDef ```tsx type ColumnDef<T> = { accessorKey?: string; accessorFn?: (row: T) => any; id?: string; header?: string | React.ComponentType; cell?: (info: CellContext<T, unknown>) => any; footer?: string | React.ComponentType; filter?: FilterType<T>; className?: string; headerClassName?: string; footerClassName?: string; enableSorting?: boolean; enableHiding?: boolean; columns?: ColumnDef<T>[]; // For grouped columns // Column sizing properties size?: number; minSize?: number; maxSize?: number; enableResizing?: boolean; }; ``` ### Filter Types #### Text Filter ```tsx { type: 'text'; field: string; placeholder: string; showList?: boolean; showTotal?: boolean; } ``` #### Range Filter ```tsx { type: 'range'; field: string; minPlaceholder?: string; maxPlaceholder?: string; showLimit?: boolean; minLimit?: number | 'faceted'; maxLimit?: number | 'faceted'; } ``` #### Select Filter ```tsx { type: 'select' | 'multi-select'; field: string; options: any[]; optionLabel: string; optionValue: string; allLabel?: string; } ``` #### Boolean Filter ```tsx { type: 'boolean'; field: string; trueLabel?: string; falseLabel?: string; allLabel?: string; } ``` #### Custom Filter ```tsx { type: "custom"; field: string; component: React.ComponentType<{ column: Column<T> }>; } ``` ## 🎨 Examples ### Basic Table with Filtering ```tsx import { DataTable, ColumnDef } from "tanstack-shadcn-table"; const columns: ColumnDef<Person>[] = [ { accessorKey: "firstName", header: "First Name", filter: { type: "text", field: "firstName", placeholder: "Search first name...", }, }, { accessorKey: "age", header: "Age", filter: { type: "range", field: "age", showLimit: true, }, }, { accessorKey: "status", header: "Status", filter: { type: "select", field: "status", options: [ { label: "Active", value: "active" }, { label: "Inactive", value: "inactive" }, ], optionLabel: "label", optionValue: "value", }, }, ]; <DataTable tableOptions={{ data, columns, filter: true, globalFilter: { show: true, }, }} />; ``` ### Table with Pagination ```tsx <DataTable tableOptions={{ data, columns, pagination: { pageSize: 10, totalRecords: 1000, pageSizeOptions: [5, 10, 20, 50], mode: "advanced", layout: ["total", "pageSize", "goto", "buttons"], pageSizeLabel: "Rows per page:", goToPageLabel: "Go to page:", totalLabel: "Total: {total} records", }, }} /> ``` ### Table with Row Selection ```tsx const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({}); <DataTable tableOptions={{ data, columns, rowSelection, onRowSelectionChange: setRowSelection, }} />; ``` ### Table with Column Reordering ```tsx const [columnOrder, setColumnOrder] = useState<string[]>([ "firstName", "lastName", "age", ]); <DataTable tableOptions={{ data, columns, reorderable: true, columnOrder, onColumnOrderChange: setColumnOrder, }} />; ``` ### Lazy Loading Table ```tsx const [data, setData] = useState<Person[]>([]); const handleLazyLoad = (event: LazyLoadEvent) => { // Fetch data from server based on event parameters fetchDataFromServer({ page: event.page, pageSize: event.rows, filters: event.filters, sorting: event.sorting, globalFilter: event.globalFilter, }).then(setData); }; <DataTable tableOptions={{ data, columns, lazy: true, onLazyLoad: handleLazyLoad, pagination: { pageSize: 20, totalRecords: 1000, }, }} />; ``` ### Grouped Columns ```tsx const columns: ColumnDef<Person>[] = [ { header: "Name", columns: [ { accessorKey: "firstName", header: "First Name", }, { accessorKey: "lastName", header: "Last Name", }, ], }, { header: "Details", columns: [ { accessorKey: "age", header: "Age", }, { accessorKey: "email", header: "Email", }, ], }, ]; ``` ### Custom Cell Rendering ```tsx const columns: ColumnDef<Person>[] = [ { accessorKey: "avatar", header: "Avatar", cell: ({ row }) => ( <img src={row.original.avatarUrl} alt="Avatar" className="w-8 h-8 rounded-full" /> ), }, { accessorKey: "status", header: "Status", cell: ({ getValue }) => { const status = getValue() as string; return ( <span className={`px-2 py-1 rounded-full text-xs ${ status === "active" ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800" }`} > {status} </span> ); }, }, ]; ``` ### Table with Column Resizing ```tsx const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({}); const columns: ColumnDef<Person>[] = [ { accessorKey: "firstName", header: "First Name", size: 200, minSize: 100, maxSize: 400, }, { accessorKey: "lastName", header: "Last Name", size: 150, enableResizing: false, // Disable resizing for this column }, { accessorKey: "email", header: "Email", size: 250, }, ]; <DataTable tableOptions={{ data, columns, enableColumnResizing: true, columnResizeMode: "onChange", // or "onEnd" columnResizeDirection: "ltr", // or "rtl" columnSizing, onColumnSizingChange: setColumnSizing, defaultColumn: { size: 150, minSize: 50, maxSize: 500, }, }} />; ``` ## 🎨 Styling The library uses Tailwind CSS classes and can be customized through: 1. **CSS Classes**: Pass custom classes through `className`, `rowClassName`, `colClassName`, etc. 2. **Custom Components**: Replace default components with your own 3. **Tailwind Configuration**: Customize the design system ### Custom Styling Example ```tsx <DataTable className="my-custom-table" tableOptions={{ data, columns, rowClassName: "hover:bg-gray-50", colClassName: "px-4 py-2", filterRowClassName: "bg-gray-100", }} /> ``` ## 🔧 Advanced Usage ### Server-Side Operations ```tsx const [tableState, setTableState] = useState({ pagination: { pageIndex: 0, pageSize: 10 }, sorting: [], columnFilters: [], globalFilter: "", }); const handleLazyLoad = async (event: LazyLoadEvent) => { const response = await fetch("/api/data", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(event), }); const result = await response.json(); setData(result.data); }; <DataTable tableOptions={{ data, columns, lazy: true, onLazyLoad: handleLazyLoad, ...tableState, onPaginationChange: (pagination) => setTableState((prev) => ({ ...prev, pagination })), onSortingChange: (sorting) => setTableState((prev) => ({ ...prev, sorting })), onColumnFiltersChange: (columnFilters) => setTableState((prev) => ({ ...prev, columnFilters })), }} />; ``` ### Custom Filter Components ```tsx const CustomDateFilter = ({ column }: { column: Column<Person> }) => { return ( <input type="date" onChange={(e) => column.setFilterValue(e.target.value)} value={(column.getFilterValue() as string) || ""} className="border rounded px-2 py-1" /> ); }; const columns: ColumnDef<Person>[] = [ { accessorKey: "birthDate", header: "Birth Date", filter: { type: "custom", field: "birthDate", component: CustomDateFilter, }, }, ]; ``` ### Column Sizing Configuration The library provides comprehensive column sizing options based on [TanStack Table's Column Sizing Guide](https://tanstack.com/table/latest/docs/guide/column-sizing): ```tsx // Default column sizing values const defaultColumnSizing = { size: 150, minSize: 20, maxSize: Number.MAX_SAFE_INTEGER, }; // Configure column sizing <DataTable tableOptions={{ data, columns, enableColumnResizing: true, columnResizeMode: "onEnd", // Recommended for large tables columnResizeDirection: "ltr", defaultColumn: { size: 200, minSize: 50, maxSize: 500, }, }} />; ``` #### Column Resize Modes - **`onEnd`** (default): Column size updates only when user finishes dragging. Better performance for complex tables. - **`onChange`**: Column size updates immediately during dragging. Provides real-time feedback. #### Performance Optimization For large tables, the library automatically implements performance optimizations: 1. **CSS Variables**: Column widths are calculated once and applied via CSS variables 2. **Memoized Calculations**: Column size calculations are memoized to prevent unnecessary re-renders 3. **Optimized Rendering**: Table body is optimized during resize operations ```tsx // Example with performance optimizations const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({}); <DataTable tableOptions={{ data: largeDataset, columns, enableColumnResizing: true, columnResizeMode: "onEnd", // Recommended for large tables columnSizing, onColumnSizingChange: setColumnSizing, }} />; ``` ## 🔒 Security The library includes comprehensive security measures to protect against common web vulnerabilities: ### Built-in Security Features #### 🛡️ **XSS Protection** All user inputs are automatically sanitized to prevent Cross-Site Scripting attacks: ```tsx import { sanitizeHtml, sanitizeSearchInput } from "tanstack-shadcn-table"; // Automatic sanitization in all filter inputs <DataTable tableOptions={{ data, columns, globalFilter: { show: true, // Global search is automatically sanitized }, }} />; ``` #### 🚦 **Rate Limiting** Built-in rate limiting prevents abuse and DoS attacks: ```tsx import { RateLimiter } from "tanstack-shadcn-table"; // Custom rate limiter for API calls const rateLimiter = new RateLimiter(100, 60000); // 100 requests per minute const handleLazyLoad = (event) => { if (!rateLimiter.isAllowed("user-123")) { console.warn("Rate limit exceeded"); return; } // Proceed with API call }; ``` #### 🔍 **Input Validation** All inputs are validated and sanitized: ```tsx // Numeric inputs are bounded const columns = [ { accessorKey: "price", filter: { type: "range", field: "price", minLimit: 0, // Automatically enforced maxLimit: 1000000, // Prevents overflow }, }, ]; ``` ### Security Utilities #### **sanitizeHtml(input: string)** Removes dangerous HTML content: ```tsx import { sanitizeHtml } from "tanstack-shadcn-table"; const safeContent = sanitizeHtml('<script>alert("xss")</script>Hello'); // Result: "Hello" ``` #### **sanitizeSearchInput(input: string)** Sanitizes search and filter inputs: ```tsx import { sanitizeSearchInput } from "tanstack-shadcn-table"; const safeSearch = sanitizeSearchInput('user"; DROP TABLE users; --'); // Result: "user DROP TABLE users " ``` #### **validatePaginationParams(pageIndex, pageSize)** Validates pagination to prevent abuse: ```tsx import { validatePaginationParams } from "tanstack-shadcn-table"; const { pageIndex, pageSize } = validatePaginationParams(-1, 999999); // Result: { pageIndex: 0, pageSize: 1000 } // Bounded values ``` #### **validateFileUpload(file: File)** Validates file uploads in custom cells: ```tsx import { validateFileUpload } from "tanstack-shadcn-table"; const CustomFileCell = ({ value }) => { const handleFileUpload = (file) => { const { isValid, error } = validateFileUpload(file); if (!isValid) { alert(error); return; } // Process safe file }; }; ``` ### Content Security Policy Use the provided CSP directives for additional security: ```tsx import { CSP_DIRECTIVES } from "tanstack-shadcn-table"; // In your HTML head or server configuration const cspHeader = Object.entries(CSP_DIRECTIVES) .map(([key, value]) => `${key} ${value}`) .join("; "); // Result: "default-src 'self'; script-src 'self' 'unsafe-inline'; ..." ``` ### Security Best Practices #### 1. **Server-Side Validation** Always validate data on the server: ```tsx // Client-side (additional layer) const handleLazyLoad = (event) => { // Client-side sanitization const sanitizedFilters = event.filters.map((filter) => ({ ...filter, value: sanitizeSearchInput(filter.value), })); // Send to server api.getData({ ...event, filters: sanitizedFilters }); }; // Server-side (primary validation) app.post("/api/data", (req, res) => { // Always validate and sanitize on server const { filters, sorting, pagination } = validateRequest(req.body); // Process request }); ``` #### 2. **Secure Custom Components** Implement security in custom components: ```tsx const SecureCustomCell = ({ value }) => { // Sanitize any user-provided content const safeValue = sanitizeHtml(String(value)); return ( <div dangerouslySetInnerHTML={{ __html: safeValue }} // Only if you need HTML rendering /> ); }; ``` #### 3. **Environment Configuration** Configure security settings based on environment: ```tsx const securityConfig = { development: { rateLimitRequests: 1000, maxDataSize: 100000, }, production: { rateLimitRequests: 100, maxDataSize: 10000, }, }; <DataTable tableOptions={{ data: data.slice(0, securityConfig[env].maxDataSize), // ... other options }} />; ``` #### 4. **Audit and Monitoring** Monitor for security events: ```tsx const secureTable = ( <DataTable tableOptions={{ data, columns, onLazyLoad: (event) => { // Log security events console.log("Data request:", { timestamp: new Date().toISOString(), filters: event.filters.length, sorting: event.sorting.length, user: getCurrentUser().id, }); handleLazyLoad(event); }, }} /> ); ``` ### Security Checklist - **Input Sanitization**: All inputs automatically sanitized - **XSS Protection**: HTML content filtered - **Rate Limiting**: Built-in request throttling - **Input Validation**: Type and range validation - **File Upload Security**: Safe file type validation - **CSP Support**: Content Security Policy helpers - ⚠️ **Server Validation**: Implement on your backend - ⚠️ **Authentication**: Implement user authentication - ⚠️ **Authorization**: Implement data access controls ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## 📄 License MIT License - see the [LICENSE](LICENSE) file for details. ## 🙏 Acknowledgments - [TanStack Table](https://tanstack.com/table) - The powerful headless table library - [shadcn/ui](https://ui.shadcn.com/) - Beautiful UI components - [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework - [Lucide React](https://lucide.dev/) - Beautiful icons ## 📞 Support If you have any questions or need help, please open an issue on GitHub. ## 🌍 Internationalization (i18n) The library includes comprehensive internationalization support with built-in translations for multiple languages and the ability to create custom translations. ### Supported Languages - **English (en)** - Default - **Turkish (tr)** - Türkçe - **Spanish (es)** - Español - **French (fr)** - Français - **German (de)** - Deutsch ### Basic Usage ```tsx import { DataTable, turkishTranslations } from "tanstack-shadcn-table"; <DataTable tableOptions={{ data, columns, translations: turkishTranslations, // Use Turkish translations }} />; ``` ### Available Translation Objects ```tsx import { defaultTranslations, // English (default) turkishTranslations, // Turkish spanishTranslations, // Spanish frenchTranslations, // French germanTranslations, // German availableLanguages, // All languages object } from "tanstack-shadcn-table"; ``` ### Dynamic Language Switching ```tsx import { availableLanguages, SupportedLanguage } from "tanstack-shadcn-table"; const [currentLanguage, setCurrentLanguage] = useState<SupportedLanguage>("en"); const translations = availableLanguages[currentLanguage].translations; <DataTable tableOptions={{ data, columns, translations, }} />; ``` ### Custom Translations Create your own translations by implementing the `TableTranslations` interface: ```tsx import { TableTranslations } from "tanstack-shadcn-table"; const customTranslations: TableTranslations = { pagination: { previous: "Geri", next: "İleri", first: "İlk", last: "Son", page: "Sayfa", of: "/", rowsPerPage: "Sayfa başına satır", goToPage: "Sayfaya git", totalRecords: "Toplam: {total} kayıt", showingXtoYofZ: "{total} kayıttan {from}-{to} arası gösteriliyor", noData: "Veri bulunamadı", }, filters: { search: "Ara", searchAllColumns: "Tüm sütunlarda ara...", showFilter: "Filtreyi Göster", hideFilter: "Filtreyi Gizle", all: "Tümü", true: "Doğru", false: "Yanlış", min: "Min", max: "Maks", // ... other filter translations }, // ... other sections }; ``` ### Translation Utilities The library provides utility functions for working with translations: ```tsx import { createTranslator, t, interpolate } from "tanstack-shadcn-table"; // Create a bound translator function const translator = createTranslator(turkishTranslations); const text = translator("pagination.next"); // "Sonraki" // Direct translation with interpolation const message = t(turkishTranslations, "pagination.totalRecords", { total: 100, }); // Result: "Toplam: 100 kayıt" // String interpolation const interpolated = interpolate("Hello {name}!", { name: "World" }); // Result: "Hello World!" ``` ### What Gets Translated The i18n system covers all user-facing text in the table: #### Pagination - Navigation buttons (Previous, Next, First, Last) - Page size selector labels - Go to page labels - Total records display - Accessibility labels #### Filters - Filter button text (Show/Hide Filter) - Global search placeholder - Filter type labels (All, True, False, Min, Max) - Range filter placeholders #### Row Selection - Checkbox accessibility labels - Selection count messages - Select all/deselect all labels #### Column Management - Column visibility controls - Resize and reorder labels - Accessibility descriptions #### Status Messages - Loading states - Error messages - No results messages ### Accessibility and i18n All accessibility labels (aria-labels, aria-descriptions) are also translated, ensuring your table is accessible in multiple languages: ```tsx // Automatically translated accessibility labels <button aria-label={t("pagination.next")}> <ChevronRightIcon /> </button> <input aria-label={t("filters.searchAllColumns")} /> ``` ### Best Practices 1. **Consistent Language**: Ensure all parts of your application use the same language 2. **Fallback**: Always provide fallback text for missing translations 3. **Context**: Consider cultural context when creating custom translations 4. **Testing**: Test your application with different languages to ensure proper layout 5. **Performance**: Translation objects are memoized for optimal performance ### Example: Complete Multilingual Setup ```tsx import { DataTable, availableLanguages, SupportedLanguage, TableTranslations, } from "tanstack-shadcn-table"; function MultilingualTable() { const [language, setLanguage] = useState<SupportedLanguage>("en"); const translations = availableLanguages[language].translations; return ( <div> {/* Language Selector */} <div className="mb-4"> {Object.entries(availableLanguages).map(([code, { name }]) => ( <button key={code} onClick={() => setLanguage(code as SupportedLanguage)} className={language === code ? "active" : ""} > {name} </button> ))} </div> {/* Table with translations */} <DataTable tableOptions={{ data, columns, translations, pagination: { pageSize: 10, totalRecords: data.length, pageSizeOptions: [5, 10, 20, 50], }, filter: true, globalFilter: { show: true }, rowSelection: {}, }} /> </div> ); } ```