Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ jobs:
name: Lint, typecheck, test, build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 20
cache: npm
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ jobs:
name: Lint, typecheck, test, build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 24

Expand All @@ -45,12 +45,12 @@ jobs:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 24

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

A summary of notable changes per release. For the full commit history see the [repository on GitHub](https://github.com/jbetancur/react-data-table-component/commits/master).

## 8.7.0

### New features

- **`ctx.error` for custom editors** — the custom editor render context now includes the current validation error (`string | null`), so custom editors can style their own invalid state when `validate` rejects a commit. → [Inline editing](/docs/inline-editing) ([#1355](https://github.com/jbetancur/react-data-table-component/issues/1355))
- **`ctx.inputRef` for custom editors** — attach it as the `ref` of your focusable element to get auto-focus when the editor opens and refocus after a rejected commit, matching the built-in editors. → [Inline editing](/docs/inline-editing) ([#1355](https://github.com/jbetancur/react-data-table-component/issues/1355))

---

## 8.6.2

### Bug fixes
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const columns = [
- [Fixed Header](https://reactdatatable.com/docs/fixed-header): sticky header with scrollable body; when to disable the responsive scroll container
- [Footer](https://reactdatatable.com/docs/footer): summary and pagination footers
- [Loading State](https://reactdatatable.com/docs/loading): progress and skeleton states
- [Inline Editing](https://reactdatatable.com/docs/inline-editing): edit cells in place
- [Inline Editing](https://reactdatatable.com/docs/inline-editing): edit cells in place with built-in editor types, per-column validation, and optimistic server-save patterns

## Styling

Expand Down
13 changes: 10 additions & 3 deletions apps/docs/src/components/demos/InlineEditingDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function InlineEditingDemo() {

const handleCellEdit = (row: Employee, value: string, column: TableColumn<Employee>) => {
const field = column.id as keyof Employee;
const parsed = field === 'salary' ? Number(value) || row.salary : field === 'remote' ? value === 'true' : value;
const parsed = field === 'salary' ? Number(value) : field === 'remote' ? value === 'true' : value;
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [field]: parsed } : r)));
setLastEdit(`Updated ${row.name} → ${String(column.name)}: "${value}"`);
};
Expand All @@ -46,6 +46,7 @@ export default function InlineEditingDemo() {
selector: r => r.name,
sortable: true,
editable: true,
validate: value => (value.trim() === '' ? 'Name is required' : true),
onCellEdit: handleCellEdit,
},
{
Expand Down Expand Up @@ -99,6 +100,10 @@ export default function InlineEditingDemo() {
sortable: true,
right: true,
editable: true,
validate: value => {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? true : 'Enter a positive number';
},
onCellEdit: handleCellEdit,
},
{
Expand All @@ -117,8 +122,10 @@ export default function InlineEditingDemo() {
<p className="text-xs text-gray-400">
Click any cell to edit. <strong>Name</strong> and <strong>Salary</strong> are text inputs;{' '}
<strong>Department</strong> and <strong>Status</strong> are dropdowns; <strong>Remote</strong> is a checkbox.{' '}
<kbd>Enter</kbd> commits, <kbd>Esc</kbd> cancels. Keyboard navigation is enabled too: click or Tab into the
table, move between cells and headers with the arrow keys, and press <kbd>Enter</kbd> to edit or sort.
<kbd>Enter</kbd> commits, <kbd>Esc</kbd> cancels. <strong>Name</strong> and <strong>Salary</strong> are
validated: try committing an empty name or a negative salary to see the inline error. Keyboard navigation is
enabled too: click or Tab into the table, move between cells and headers with the arrow keys, and press{' '}
<kbd>Enter</kbd> to edit or sort.
</p>
<DataTable columns={columns} data={data} highlightOnHover cellNavigation />
{lastEdit && <div className="text-xs text-emerald-600 font-mono">{lastEdit}</div>}
Expand Down
93 changes: 93 additions & 0 deletions apps/docs/src/components/demos/ServerSideEditDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import DataTable from '../ThemedDataTable';
import { type TableColumn } from 'react-data-table-component';

interface Product {
id: number;
sku: string;
name: string;
stock: number;
price: number;
}

const initialData: Product[] = [
{ id: 1, sku: 'CH-114', name: 'Aeron Chair', stock: 12, price: 745 },
{ id: 2, sku: 'DK-208', name: 'Standing Desk', stock: 7, price: 890 },
{ id: 3, sku: 'MN-330', name: '32" Monitor', stock: 24, price: 429 },
{ id: 4, sku: 'KB-501', name: 'Mech Keyboard', stock: 41, price: 159 },
{ id: 5, sku: 'LM-612', name: 'Desk Lamp', stock: 63, price: 49 },
];

function saveToServer(field: keyof Product, value: string | number): Promise<void> {
return new Promise((resolve, reject) =>
setTimeout(() => {
if (field === 'price' && Number(value) > 1000) reject(new Error('Price exceeds the $1,000 cap'));
else resolve();
}, 900),
);
}

export default function ServerSideEditDemo() {
const [data, setData] = React.useState<Product[]>(initialData);
const [savingId, setSavingId] = React.useState<number | null>(null);
const [status, setStatus] = React.useState<{ ok: boolean; message: string } | null>(null);

const handleCellEdit = async (row: Product, value: string, column: TableColumn<Product>) => {
const field = column.id as keyof Product;
const parsed = field === 'name' ? value : Number(value);
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [field]: parsed } : r)));
setSavingId(row.id);
setStatus({ ok: true, message: `Saving ${row.sku}…` });
try {
await saveToServer(field, parsed);
setStatus({ ok: true, message: `Saved ${row.sku} → ${String(column.name)}: "${value}"` });
} catch (err) {
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [field]: row[field] } : r)));
setStatus({ ok: false, message: `${(err as Error).message} — rolled back ${row.sku}` });
} finally {
setSavingId(null);
}
};

const columns: TableColumn<Product>[] = [
{ id: 'sku', name: 'SKU', selector: r => r.sku, width: '90px' },
{ id: 'name', name: 'Product', selector: r => r.name, editable: true, onCellEdit: handleCellEdit },
{
id: 'stock',
name: 'Stock',
selector: r => r.stock,
right: true,
editor: { type: 'number', min: 0, step: 1 },
validate: value => (Number.isInteger(Number(value)) && Number(value) >= 0 ? true : 'Enter a whole number'),
onCellEdit: handleCellEdit,
},
{
id: 'price',
name: 'Price',
selector: r => r.price,
format: r => `$${r.price.toLocaleString()}`,
right: true,
editor: { type: 'number', min: 0, step: 1 },
validate: value => (Number(value) > 0 ? true : 'Enter a positive number'),
onCellEdit: handleCellEdit,
},
];

return (
<div className="space-y-2">
<p className="text-xs text-gray-400">
Edits apply optimistically, then save to a simulated server with ~1s latency. The row dims while the save is in
flight. The server rejects any <strong>Price</strong> over $1,000 — try it to watch the edit roll back.
</p>
<DataTable
columns={columns}
data={data}
highlightOnHover
conditionalRowStyles={[{ when: r => r.id === savingId, style: { opacity: 0.45 } }]}
/>
{status && (
<div className={`text-xs font-mono ${status.ok ? 'text-emerald-600' : 'text-red-600'}`}>{status.message}</div>
)}
</div>
);
}
2 changes: 2 additions & 0 deletions apps/docs/src/pages/docs/api.astro
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ interface CustomCellEditorContext<T> {
commit: (value?: string) => void;
cancel: () => void;
column: TableColumn<T>;
error: string | null;
inputRef: React.RefCallback<HTMLElement>;
}`} lang="ts" />

<DocsTable
Expand Down
87 changes: 82 additions & 5 deletions apps/docs/src/pages/docs/inline-editing.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
import Demo from '../../components/Demo.astro';
import CodeBlock from '../../components/CodeBlock.astro';
import InlineEditingDemo from '../../components/demos/InlineEditingDemo.tsx';
import ServerSideEditDemo from '../../components/demos/ServerSideEditDemo.tsx';
import DocsTable from '../../components/DocsTable.astro';
---

Expand Down Expand Up @@ -30,7 +31,7 @@ import DocsTable from '../../components/DocsTable.astro';

<Demo
title="Editable cells — text + dropdown"
description="Name and Salary use text inputs. Department and Status use dropdowns. Click any cell to edit."
description="Name and Salary use text inputs with validation. Department and Status use dropdowns. Click any cell to edit."
code={`import { useState } from 'react';
import DataTable, { type TableColumn } from 'react-data-table-component';

Expand All @@ -48,13 +49,20 @@ export default function App() {
const handleCellEdit = (row: Employee, value: string, column: TableColumn<Employee>) => {
const field = column.id as keyof Employee;
setData(prev =>
prev.map(r => (r.id === row.id ? { ...r, [field]: field === 'salary' ? Number(value) || r.salary : value } : r)),
prev.map(r => (r.id === row.id ? { ...r, [field]: field === 'salary' ? Number(value) : value } : r)),
);
};

const columns: TableColumn<Employee>[] = [
// Text editor — editable: true is shorthand for { editor: { type: 'text' } }
{ id: 'name', name: 'Name', selector: r => r.name, editable: true, onCellEdit: handleCellEdit },
{
id: 'name',
name: 'Name',
selector: r => r.name,
editable: true,
validate: value => (value.trim() === '' ? 'Name is required' : true),
onCellEdit: handleCellEdit,
},

// Dropdown editor
{
Expand Down Expand Up @@ -96,6 +104,10 @@ export default function App() {
format: r => \`$\${r.salary.toLocaleString()}\`,
right: true,
editable: true,
validate: value => {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? true : 'Enter a positive number';
},
onCellEdit: handleCellEdit,
},
];
Expand Down Expand Up @@ -216,9 +228,9 @@ export default function App() {
id: 'role', name: 'Role', selector: r => r.role,
editor: {
type: 'custom',
render: ({ value, setValue, commit, cancel, row }) => (
render: ({ value, setValue, commit, cancel, row, inputRef }) => (
<Autocomplete
autoFocus
ref={inputRef}
value={value}
onChange={setValue}
onSelect={v => commit(v)}
Expand All @@ -236,6 +248,17 @@ export default function App() {
your custom editor only needs to render the input.
</p>

<p>
Validation works the same as for built-in editors: <code>ctx.commit</code> runs the
column's <code>validate</code> first, and a string result keeps the editor open with the
inline error tooltip. Since 8.7.0 the render context also carries the current error as
<code>ctx.error</code> (<code>string | null</code>), so your editor can style its own
invalid state, e.g. <code>aria-invalid=&#123;!!ctx.error&#125;</code>. Attach
<code>ctx.inputRef</code> as the <code>ref</code> of your focusable element to get the
same focus handling as the built-in editors: auto-focus when the editor opens and
refocus after a rejected commit.
</p>

<h2>The cell becomes the editor</h2>

<p>
Expand Down Expand Up @@ -293,6 +316,60 @@ export default function App() {
}
};`} />

<Demo
title="Server-side saves — optimistic update with rollback"
description="Edits apply immediately and save to a simulated server. Failed saves roll back. The server rejects prices over $1,000."
code={`import { useState } from 'react';
import DataTable, { type TableColumn } from 'react-data-table-component';

export default function App() {
const [data, setData] = useState<Product[]>(initialData);
const [savingId, setSavingId] = useState<number | null>(null);

const handleCellEdit = async (row: Product, value: string, column: TableColumn<Product>) => {
const field = column.id as keyof Product;
const parsed = field === 'name' ? value : Number(value);

// Optimistic update
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [field]: parsed } : r)));
setSavingId(row.id);

try {
await api.updateProduct(row.id, { [field]: parsed });
} catch (err) {
// Roll back the field to its pre-edit value
setData(prev => prev.map(r => (r.id === row.id ? { ...r, [field]: row[field] } : r)));
} finally {
setSavingId(null);
}
};

const columns: TableColumn<Product>[] = [
{ id: 'sku', name: 'SKU', selector: r => r.sku },
{ id: 'name', name: 'Product', selector: r => r.name, editable: true, onCellEdit: handleCellEdit },
{
id: 'price',
name: 'Price',
selector: r => r.price,
right: true,
editor: { type: 'number', min: 0 },
validate: value => (Number(value) > 0 ? true : 'Enter a positive number'),
onCellEdit: handleCellEdit,
},
];

return (
<DataTable
columns={columns}
data={data}
conditionalRowStyles={[{ when: r => r.id === savingId, style: { opacity: 0.45 } }]}
/>
);
}`}
>
<ServerSideEditDemo client:load />
</Demo>

<h2>Validation</h2>
<p>
Add a <code>validate</code> function to any column to gate the edit before
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-data-table-component",
"version": "8.6.2",
"version": "8.7.0",
"description": "A fast, feature-rich React data table. Working table in 10 lines.",
"funding": [
{
Expand Down
Loading