Skip to content

EditableTableCell

Inline editable table cell component for quick data updates within tables.

Import

tsx
import { EditableTableCell } from '@/shared/ui/components/table/EditableTableCell'

Props

EditableTableCellProps

PropTypeDefaultDescription
Value
valuestring | numberRequiredCurrent value to display and edit
Callback
onSave(value: string | number) => voidRequiredCalled when value is saved
Input Configuration
type'text' | 'number''text'Input type
prefixstring-Prefix to display before value (e.g., "$" for currency)
Validation
validate(value: string | number) => boolean-Custom validation function. Return false to prevent save
Styling
classNamestring-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 NaN values
  • 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

  1. Trigger: Click on the cell to enter edit mode
  2. Input: Shows input field with current value
  3. Actions: Save (✓) and Cancel (✗) buttons appear
  4. Keyboard:
    • Enter: Save changes
    • Escape: Cancel changes

Display Mode

  • Shows value with optional prefix
  • Hover effect indicates editability
  • Click anywhere on cell to edit

Validation Flow

  1. User enters new value
  2. Built-in validation runs (empty check for text, NaN check for numbers)
  3. Custom validation runs (if provided)
  4. If validation passes, onSave is called
  5. 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