Appearance
EditableTableCell
Inline editable table cell component for quick data updates within tables.
Import
tsx
import { EditableTableCell } from '@/shared/ui/components/table/EditableTableCell'Props
EditableTableCellProps
| Prop | Type | Default | Description |
|---|---|---|---|
| Value | |||
value | string | number | Required | Current value to display and edit |
| Callback | |||
onSave | (value: string | number) => void | Required | Called when value is saved |
| Input Configuration | |||
type | 'text' | 'number' | 'text' | Input type |
prefix | string | - | Prefix to display before value (e.g., "$" for currency) |
| Validation | |||
validate | (value: string | number) => boolean | - | Custom validation function. Return false to prevent save |
| Styling | |||
className | string | - | Additional CSS classes for container |
Built-in Validation
The component includes automatic validation:
Text Type
- Prevents saving empty strings (after trimming)
- Minimum length: 1 character
Number Type
- Prevents saving
NaNvalues - Prevents saving negative numbers
- Automatically formats display with 2 decimal places
Examples
Basic Text Editing
tsx
import { EditableTableCell } from '@/shared/ui/components/table/EditableTableCell'
function ProductNameCell({ product, onUpdate }) {
return (
<EditableTableCell
value={product.name}
type="text"
onSave={(newName) => onUpdate(product.id, { name: newName })}
/>
)
}Number with Prefix
tsx
function PriceCell({ product, onUpdate }) {
return (
<EditableTableCell
value={product.price}
type="number"
prefix="$"
onSave={(newPrice) => onUpdate(product.id, { price: newPrice })}
/>
)
}With Custom Validation
tsx
function StockCell({ product, onUpdate }) {
return (
<EditableTableCell
value={product.stock}
type="number"
onSave={(newStock) => onUpdate(product.id, { stock: newStock })}
validate={(value) => {
const num = Number(value)
return num >= 0 && num <= 10000 // Max stock limit
}}
/>
)
}In DataTable
tsx
import { DataTable, type DataTableColumn } from '@/shared/ui/components/table/DataTable'
import { EditableTableCell } from '@/shared/ui/components/table/EditableTableCell'
type Product = {
id: number
name: string
price: number
stock: number
sku: string
}
const columns: DataTableColumn<Product>[] = [
{
id: 'sku',
header: 'SKU',
cell: (row) => row.sku,
},
{
id: 'name',
header: 'Product Name',
cell: (row) => (
<EditableTableCell
value={row.name}
type="text"
onSave={(newName) => updateProduct(row.id, { name: newName })}
validate={(value) => String(value).length >= 3}
/>
),
},
{
id: 'price',
header: 'Price',
cell: (row) => (
<EditableTableCell
value={row.price}
type="number"
prefix="$"
onSave={(newPrice) => updateProduct(row.id, { price: newPrice })}
validate={(value) => Number(value) > 0 && Number(value) < 10000}
/>
),
},
{
id: 'stock',
header: 'Stock',
cell: (row) => (
<EditableTableCell
value={row.stock}
type="number"
onSave={(newStock) => updateProduct(row.id, { stock: newStock })}
/>
),
},
]
function ProductsTable({ products }: { products: Product[] }) {
return <DataTable columns={columns} data={products} />
}With Async Save
tsx
function AsyncEditableCell({ value, productId, field }) {
const [isSaving, setIsSaving] = useState(false)
const handleSave = async (newValue) => {
setIsSaving(true)
try {
await updateProductField(productId, field, newValue)
toast.success('Updated successfully')
} catch (error) {
toast.error('Failed to update')
} finally {
setIsSaving(false)
}
}
return (
<EditableTableCell
value={value}
onSave={handleSave}
className={isSaving ? 'opacity-50' : ''}
/>
)
}Percentage Input
tsx
function DiscountCell({ product, onUpdate }) {
return (
<EditableTableCell
value={product.discount}
type="number"
prefix="%"
onSave={(newDiscount) => onUpdate(product.id, { discount: newDiscount })}
validate={(value) => {
const num = Number(value)
return num >= 0 && num <= 100
}}
/>
)
}With Custom Styling
tsx
<EditableTableCell
value={product.name}
onSave={handleSave}
className="font-medium text-primary"
/>Behavior
Edit Mode
- Trigger: Click on the cell to enter edit mode
- Input: Shows input field with current value
- Actions: Save (✓) and Cancel (✗) buttons appear
- Keyboard:
Enter: Save changesEscape: Cancel changes
Display Mode
- Shows value with optional prefix
- Hover effect indicates editability
- Click anywhere on cell to edit
Validation Flow
- User enters new value
- Built-in validation runs (empty check for text, NaN check for numbers)
- Custom validation runs (if provided)
- If validation passes,
onSaveis called - If validation fails, save is prevented (no feedback shown)
Styling
The component uses the following structure:
tsx
// Display mode
<EditableTrigger>
{prefix && <span className="text-muted-foreground">{prefix}</span>}
<span>{formattedValue}</span>
</EditableTrigger>
// Edit mode
<div className="flex items-center gap-2">
<Input type={type} value={draft} />
<EditableActions onSave={save} onCancel={cancel} />
</div>Accessibility
- Input receives focus automatically when entering edit mode
- Save/Cancel buttons have proper labels
- Keyboard navigation supported (Enter/Escape)
Integration with InlineEditable
EditableTableCell is built on top of the InlineEditable primitive:
tsx
// Simplified implementation
export const EditableTableCell = ({ value, onSave, type, prefix, validate }) => {
return (
<InlineEditable
value={value}
onSave={(newValue) => {
// Built-in validation
if (type === 'text' && String(newValue).trim() === '') return
if (type === 'number' && (isNaN(Number(newValue)) || Number(newValue) < 0)) return
// Custom validation
if (validate && !validate(newValue)) return
onSave(newValue)
}}
renderDisplay={({ onStartEdit }) => (
<EditableTrigger onClick={onStartEdit}>
{prefix && <span>{prefix}</span>}
<span>{type === 'number' ? Number(value).toFixed(2) : value}</span>
</EditableTrigger>
)}
renderEditor={({ value: draft, onChange, onSave, onCancel, inputProps }) => (
<div className="flex items-center gap-2">
<Input
type={type}
value={draft}
onChange={(e) => onChange(type === 'number' ? Number(e.target.value) : e.target.value)}
{...inputProps}
/>
<EditableActions onSave={onSave} onCancel={onCancel} />
</div>
)}
/>
)
}Best Practices
✅ Do's
- Provide validation for critical fields
- Show feedback after save (toast notifications)
- Handle errors gracefully
- Use appropriate types (number for numeric values)
- Add prefixes for units ($, %, etc.)
❌ Don'ts
- Don't use for complex inputs (use a modal/form instead)
- Don't forget validation for important data
- Don't use for read-only data (just display the value)
- Don't use for large text (use textarea in a modal)
See Also
- InlineEditable API - Base primitive
- DataTable API - Table component
- Form Components - For complex inputs