UNPKG

@talberos/custom-table

Version:

Advanced Excel-style table component for React with inline editing, cell selection, filters and more

688 lines (545 loc) 19.1 kB
# @talberos/custom-table Advanced Excel-style table component for React with inline editing, cell selection, filters, sorting and more. [![npm version](https://img.shields.io/npm/v/@talberos/custom-table.svg)](https://www.npmjs.com/package/@talberos/custom-table) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) --- ## Table of Contents - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [Component Props](#component-props) - [Column Definition](#column-definition) - [Row Identifiers](#row-identifiers) - [Column Types](#column-types) - [Edit Types](#edit-types) - [Supported Countries](#supported-countries) - [Practical Examples](#practical-examples) - [Editing and Persistence](#editing-and-persistence) - [Dynamic Dropdowns](#dynamic-dropdowns) - [Context Menu](#context-menu) - [Data Export](#data-export) - [Customization](#customization) - [Keyboard Shortcuts](#keyboard-shortcuts) - [API Reference](#api-reference) --- ## Features | Feature | Description | |---------|-------------| | **Excel-like Selection** | Click, drag, Shift+Click for ranges | | **Inline Editing** | Double click to edit any cell | | **15 Column Types** | text, numeric, badge, country, date, datetime, link, email, phone, boolean, rating, progress, heatmap, sparkline, avatar | | **5 Edit Types** | text, numeric, select, date, boolean | | **Smart Dropdowns** | With search, flags and dynamic creation | | **67 Countries with Flags** | Americas, Europe, Asia, Africa and Oceania | | **Auto-scroll** | Automatic scroll when selecting near edges | | **Resizable Columns** | Drag column borders to resize | | **Sorting** | Click headers to sort ASC/DESC | | **Global Filter** | Search across all columns | | **Pagination** | 50, 100, 200, 500 or all rows | | **Context Menu** | Right-click to copy, hide columns/rows | | **Copy Cells** | Ctrl/Cmd + C to copy selection | | **Export** | CSV and Excel | | **Themes** | Automatic dark/light mode | | **Mobile-first** | Optimized for touch | | **TypeScript** | Full typing | --- ## Installation ```bash npm install @talberos/custom-table ``` ### Peer Dependencies ```bash npm install react react-dom @mui/material @emotion/react @emotion/styled @tanstack/react-table ``` --- ## Quick Start ```tsx 'use client' import CustomTable from '@talberos/custom-table' import type { ColumnDef } from '@talberos/custom-table' const columns: ColumnDef[] = [ { accessorKey: 'id', header: 'ID', width: 60 }, { accessorKey: 'name', header: 'Name', editable: true }, { accessorKey: 'email', header: 'Email', type: 'email' }, { accessorKey: 'status', header: 'Status', type: 'badge' }, ] const data = [ { id: 1, name: 'John Doe', email: 'john@mail.com', status: 'Active' }, { id: 2, name: 'Jane Smith', email: 'jane@mail.com', status: 'Pending' }, ] export default function MyTable() { return ( <CustomTable data={data} columnsDef={columns} containerHeight="500px" /> ) } ``` --- ## Component Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `data` | `any[]` | **required** | Array of objects with data | | `columnsDef` | `ColumnDef[]` | **required** | Column definitions | | `onCellUpdate` | `(rowId, colId, value) => Promise<void>` | - | Callback when editing cell | | `onExportCSV` | `(data) => void` | - | CSV export handler | | `onExportExcel` | `(data) => void` | - | Excel export handler | | `loadingText` | `string` | `'Loading data...'` | Loading text | | `noResultsText` | `string` | `'No results found'` | No data text | | `enableFilters` | `boolean` | `true` | Enable global filter | | `enableColumnFilters` | `boolean` | `true` | Enable column filters | | `enableSorting` | `boolean` | `true` | Enable sorting | | `enablePagination` | `boolean` | `true` | Enable pagination | | `enableCellSelection` | `boolean` | `true` | Enable selection | | `enableCellEditing` | `boolean` | `true` | Enable editing | | `enableExport` | `boolean` | `true` | Show export button | | `defaultPageSize` | `number` | `100` | Rows per page | | `rowHeight` | `number` | `36` | Row height (px) | | `containerHeight` | `string \| number` | `'80vh'` | Container height | | `defaultTheme` | `'light' \| 'dark' \| 'system'` | `'light'` | Default theme | | `className` | `string` | - | Custom CSS class | | `style` | `CSSProperties` | - | Inline styles | --- ## Column Definition ```typescript interface ColumnDef { accessorKey: string // Field key in data header: string // Header text type?: ColumnType // Render type width?: number // Initial width (default: 150) minWidth?: number // Minimum width (default: 50) maxWidth?: number // Maximum width (default: 800) editable?: boolean // Allow editing editType?: EditType // 'text' | 'numeric' | 'select' | 'date' | 'boolean' options?: SelectOption[] // Options for select allowCreate?: boolean // Create new options in select onCreateOption?: (value: string) => Promise<void> isNumeric?: boolean // Indicates numeric (auto-detected if type: 'numeric') isRowId?: boolean // Mark this column as unique row identifier precision?: number // Decimals for numeric min?: number // Minimum value for numeric max?: number // Maximum value for numeric textAlign?: 'left' | 'center' | 'right' badgeColors?: Record<string, { bg: string; text: string }> sortable?: boolean // Allow sorting (default: true) filterable?: boolean // Allow filtering (default: true) hidden?: boolean // Hide column render?: (value: any, row: any) => React.ReactNode } ``` --- ## Row Identifiers (Row IDs) CustomTable automatically handles row identifiers to optimize performance and avoid conflicts with your business `id` field. ### Automatic Detection The table automatically detects which field to use as unique identifier following this priority order: 1. **Explicitly marked column** with `isRowId: true` 2. **Column with `accessorKey: 'id'`** (automatic detection) 3. **Auto-generated IDs** based on index (fallback) ### Use Cases #### ✅ Case 1: Using 'id' field from your database (RECOMMENDED) If your data already has a unique `id` field, simply include it in the columns: ```tsx const columns: ColumnDef[] = [ { accessorKey: 'id', header: 'ID', width: 80 }, // Automatically detected { accessorKey: 'name', header: 'Name', editable: true }, { accessorKey: 'email', header: 'Email', type: 'email' }, ] const data = [ { id: 123, name: 'John Doe', email: 'john@mail.com' }, { id: 456, name: 'Jane Smith', email: 'jane@mail.com' }, ] // Table automatically uses 'id' field as internal identifier // You can edit, filter and sort by 'id' without issues // onCellUpdate will receive rowId='123' when editing first row ``` #### ✅ Case 2: ID field with different name If your identifier is called `userId`, `productId`, etc.: ```tsx const columns: ColumnDef[] = [ { accessorKey: 'userId', header: 'User ID', width: 80, isRowId: true }, // Mark explicitly { accessorKey: 'name', header: 'Name', editable: true }, ] const data = [ { userId: 'usr_123', name: 'John Doe' }, { userId: 'usr_456', name: 'Jane Smith' }, ] // Table uses 'userId' as internal identifier // onCellUpdate will receive rowId='usr_123' when editing first row ``` #### ✅ Case 3: Data without unique identifier If your data doesn't have an ID field: ```tsx const columns: ColumnDef[] = [ { accessorKey: 'name', header: 'Name', editable: true }, { accessorKey: 'email', header: 'Email', type: 'email' }, ] const data = [ { name: 'John Doe', email: 'john@mail.com' }, { name: 'Jane Smith', email: 'jane@mail.com' }, ] // Table generates IDs automatically: '0', '1', '2', etc. // ⚠️ onCellUpdate will receive rowId='0' for first row // ⚠️ NOT recommended if you need to sync with backend ``` ### Backend Persistence When you edit a cell, `onCellUpdate` receives the row ID: ```tsx const handleCellUpdate = async (rowId: string, colId: string, value: string) => { // rowId is the value of the field marked as ID // If using 'id' rowId will be '123', '456', etc. // If using 'userId' with isRowId: true rowId will be 'usr_123', 'usr_456', etc. // If no ID rowId will be '0', '1', '2', etc. (index) await fetch(`/api/items/${rowId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [colId]: value }) }) // Update local state setData(prev => prev.map(item => item.id === Number(rowId) ? { ...item, [colId]: value } : item )) } ``` ### Best Practices | Scenario | Recommendation | |----------|----------------| | **API/Database data** | Include `id` field in data and columns | | **Different identifier** | Use `isRowId: true` on corresponding column | | **Static data without ID** | ⚠️ Add sequential `id` before passing to table | | **Multiple tables** | Each table can have its own ID field | --- ## Column Types | Type | Description | Example | |------|-------------|---------| | `text` | Plain text (default) | `"Hello world"` | | `numeric` | Formatted number | `1234.56` | | `badge` | Colored label | `"Active"` | | `country` | Flag + name | `"Argentina"` | | `date` | Date | `"2024-01-15"` | | `datetime` | Date and time | `"2024-01-15T10:30"` | | `link` | Clickable URL | `"https://..."` | | `email` | Email with mailto | `"email@mail.com"` | | `phone` | Phone with tel: | `"+1 234 5678"` | | `boolean` | Visual indicator | `true` / `false` | | `rating` | Stars (1-5) | `4` | | `progress` | Progress bar | `75` | | `heatmap` | Color by value | `50` | | `sparkline` | Mini chart | `[10, 20, 15]` | | `avatar` | Circular image | `"https://...jpg"` | --- ## Edit Types | EditType | Description | |----------|-------------| | `text` | Free text (textarea) | | `numeric` | Numbers only with min/max | | `select` | Dropdown with search | | `date` | Date picker | | `boolean` | Visual toggle | --- ## Supported Countries **67 countries** with flags organized by region: ### Americas (22) Argentina, Bolivia, Brazil, Canada, Chile, Colombia, Costa Rica, Cuba, Ecuador, El Salvador, United States, Guatemala, Honduras, Mexico, Nicaragua, Panama, Paraguay, Peru, Puerto Rico, Dominican Republic, Uruguay, Venezuela ### Europe (22) Germany, Austria, Belgium, Denmark, Spain, Finland, France, Greece, Hungary, Ireland, Italy, Norway, Netherlands, Poland, Portugal, United Kingdom, Czech Republic, Romania, Russia, Sweden, Switzerland, Ukraine ### Asia (17) Saudi Arabia, China, South Korea, United Arab Emirates, Philippines, Hong Kong, India, Indonesia, Israel, Japan, Malaysia, Pakistan, Singapore, Thailand, Taiwan, Turkey, Vietnam ### Africa and Oceania (6) Australia, Egypt, Morocco, Nigeria, New Zealand, South Africa ```tsx { accessorKey: 'country', header: 'Country', type: 'country', editable: true, editType: 'select', options: [ { value: 'Argentina', label: 'Argentina' }, { value: 'Mexico', label: 'Mexico' }, { value: 'Spain', label: 'Spain' }, // ... use exactly these names ] } ``` --- ## Practical Examples ### Static Data (JSON) ```tsx 'use client' import CustomTable from '@talberos/custom-table' import type { ColumnDef } from '@talberos/custom-table' const products = [ { id: 1, name: 'Laptop', price: 999.99, stock: 15, status: 'Available' }, { id: 2, name: 'Mouse', price: 29.99, stock: 150, status: 'Available' }, { id: 3, name: 'Keyboard', price: 79.99, stock: 0, status: 'Out of Stock' }, ] const columns: ColumnDef[] = [ { accessorKey: 'id', header: 'ID', width: 60 }, { accessorKey: 'name', header: 'Product', editable: true }, { accessorKey: 'price', header: 'Price', type: 'numeric', precision: 2 }, { accessorKey: 'stock', header: 'Stock', type: 'numeric' }, { accessorKey: 'status', header: 'Status', type: 'badge', badgeColors: { 'Available': { bg: '#D1FAE5', text: '#059669' }, 'Out of Stock': { bg: '#FEE2E2', text: '#DC2626' }, } }, ] export default function ProductsTable() { return ( <CustomTable data={products} columnsDef={columns} containerHeight="600px" rowHeight={32} /> ) } ``` ### Fetch from REST API ```tsx 'use client' import { useState, useEffect } from 'react' import CustomTable from '@talberos/custom-table' import type { ColumnDef } from '@talberos/custom-table' const columns: ColumnDef[] = [ { accessorKey: 'id', header: 'ID', width: 60 }, { accessorKey: 'name', header: 'Name', editable: true }, { accessorKey: 'email', header: 'Email', type: 'email' }, { accessorKey: 'phone', header: 'Phone', type: 'phone' }, ] export default function UsersTable() { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { fetch('https://jsonplaceholder.typicode.com/users') .then(res => res.json()) .then(data => { setUsers(data) setLoading(false) }) }, []) const handleCellUpdate = async (rowId: string, colId: string, value: string) => { await fetch(`/api/users/${rowId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [colId]: value }) }) setUsers(prev => prev.map(u => u.id === Number(rowId) ? { ...u, [colId]: value } : u) ) } if (loading) return <div>Loading...</div> return ( <CustomTable data={users} columnsDef={columns} onCellUpdate={handleCellUpdate} containerHeight="500px" /> ) } ``` --- ## Editing and Persistence ```tsx const handleCellUpdate = async (rowId: string, colId: string, value: string) => { try { // 1. Save to backend await fetch(`/api/items/${rowId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [colId]: value }) }) // 2. Update local state setData(prev => prev.map(item => item.id === Number(rowId) ? { ...item, [colId]: value } : item )) } catch (error) { console.error('Error:', error) } } ``` --- ## Dynamic Dropdowns ### Basic Select ```tsx { accessorKey: 'status', header: 'Status', type: 'badge', editable: true, editType: 'select', options: [ { value: 'Active', label: 'Active' }, { value: 'Inactive', label: 'Inactive' }, ] } ``` ### Select with Dynamic Creation ```tsx { accessorKey: 'category', header: 'Category', type: 'badge', editable: true, editType: 'select', options: categories, allowCreate: true, onCreateOption: async (newValue) => { await fetch('/api/categories', { method: 'POST', body: JSON.stringify({ name: newValue }) }) setCategories(prev => [...prev, { value: newValue, label: newValue }]) } } ``` --- ## Context Menu Right-click on table: | Option | Description | |--------|-------------| | **Copy** | Copy selected cells | | **Hide column** | Hide the column | | **Hide row** | Hide the row | --- ## Data Export ### Default Table includes automatic CSV export button. ### Custom ```tsx const handleExportCSV = (data: any[]) => { // Your custom logic } <CustomTable data={data} columnsDef={columns} onExportCSV={handleExportCSV} /> ``` --- ## Customization ### Badge Colors ```tsx { accessorKey: 'priority', header: 'Priority', type: 'badge', badgeColors: { 'High': { bg: '#FEE2E2', text: '#DC2626' }, 'Medium': { bg: '#FEF3C7', text: '#D97706' }, 'Low': { bg: '#D1FAE5', text: '#059669' }, } } ``` ### Custom Rendering ```tsx { accessorKey: 'actions', header: 'Actions', render: (value, row) => ( <button onClick={() => handleEdit(row.id)}>Edit</button> ) } ``` ### Compact Rows ```tsx <CustomTable data={data} columnsDef={columns} rowHeight={28} defaultPageSize={100} /> ``` --- ## Keyboard Shortcuts | Key | Action | |-----|--------| | `↑ →` | Navigate between cells | | `Shift + Arrows` | Extend selection | | `Ctrl/Cmd + C` | Copy cells | | `Enter` | Confirm edit | | `Tab` | Next cell | | `Escape` | Cancel edit | | `Double click` | Start editing | --- ## API Reference ```tsx import CustomTable, { type ColumnDef, type CustomTableProps, type ColumnType, type EditType, type SelectOption, buildColumns, THEME_COLORS, COLUMN_CONFIG, STYLES_CONFIG, TableEditContext, useTableEdit, } from '@talberos/custom-table' ``` --- ## Requirements - React 18+ - @mui/material 5+ - @tanstack/react-table 8+ --- ## Column Filters Filters are automatically shown below each header according to column type: | Column Type | Filter Type | |-------------|-------------| | `text`, `email`, `phone`, `link`, `country` | Text input | | `numeric`, `rating`, `progress`, `heatmap` | Min-Max range | | `badge` (with options) | Options dropdown | | `boolean` | Yes/No dropdown | | `date`, `datetime` | Date picker | --- ## Project Structure ``` src/ ├── index.tsx # Main CustomTable component ├── types.ts # TypeScript types ├── config.ts # Configuration (sizes, styles) ├── CustomTableColumnsConfig.tsx # Column rendering and flags ├── theme/ └── colors.ts # Theme colors ├── hooks/ ├── useThemeMode.ts # Light/dark theme handling ├── useCustomTableLogic.ts # Export logic └── useCellEditingOrchestration.ts # Edit orchestration └── TableView/ ├── index.tsx # Main table view ├── hooks/ ├── useCellSelection.ts # Cell selection and auto-scroll ├── useColumnResize.ts # Resize columns ├── useInlineCellEdit.ts # Inline editing ├── useClipboardCopy.ts # Copy to clipboard └── useTableViewContextMenu.ts # Context menu ├── logic/ ├── domUtils.ts # DOM utilities ├── selectionLogic.ts # Selection logic └── dragLogic.ts # Drag logic └── subcomponents/ ├── TableHeader.tsx # Headers and filters ├── TableBody.tsx # Table body ├── Pagination.tsx # Pagination ├── ContextualMenu.tsx # Context menu ├── CustomSelectDropdown.tsx # Custom dropdown ├── LoadingOverlay.tsx # Loading overlay └── NoResultsOverlay.tsx # No results overlay ``` --- ## License MIT --- ## Author **Gabriel Hércules Miguel** - LinkedIn: [gabrielherculesmiguel](https://linkedin.com/in/gabrielherculesmiguel) - GitHub: [@gabrielmiguelok](https://github.com/gabrielmiguelok) --- ## Repository [https://github.com/gabrielmiguelok/customtable](https://github.com/gabrielmiguelok/customtable)