Cell and row editing with validation.
The ActiveGrid supports inline editing with two modes: cell editing and row editing. Both modes support validation, error handling, and asynchronous commit operations.
<ActiveGrid
editMode="cell" // or "row"
onDataCommit={async (payload) => {
await saveData(payload.newData);
}}
{...}
/>Edit individual cells. Click a cell to enter edit mode, make changes, and press Enter or click outside to commit.
<ActiveGrid
editMode="cell"
columns={columns}
data={data}
onDataCommit={handleCommit}
{...}
/>Edit entire rows at once. Double-click a row to enter edit mode, modify multiple cells, then commit all changes together.
<ActiveGrid
editMode="row"
columns={columns}
data={data}
onDataCommit={handleCommit}
{...}
/>Enable editing per column using the editable property in column metadata:
{
accessorKey: 'name',
header: 'Name',
meta: {
editable: true,
editType: 'text',
},
}{
accessorKey: 'name',
header: 'Name',
meta: {
editable: true,
editType: 'text',
},
}{
accessorKey: 'age',
header: 'Age',
meta: {
editable: true,
editType: 'number',
},
}{
accessorKey: 'birthDate',
header: 'Birth Date',
meta: {
editable: true,
editType: 'date',
},
}{
accessorKey: 'status',
header: 'Status',
meta: {
editable: true,
editType: 'select',
editOptions: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Pending', value: 'pending' },
],
},
}{
accessorKey: 'isActive',
header: 'Active',
meta: {
editable: true,
editType: 'boolean',
},
}Enable editing based on row data:
{
accessorKey: 'name',
header: 'Name',
meta: {
editable: (row) => row.status !== 'locked',
editType: 'text',
},
}{
accessorKey: 'email',
header: 'Email',
meta: {
editable: true,
editType: 'text',
validate: (value) => {
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Invalid email format';
}
return undefined;
},
},
}{
accessorKey: 'username',
header: 'Username',
meta: {
editable: true,
editType: 'text',
validate: async (value, row) => {
if (!value) return 'Username is required';
const exists = await checkUsernameExists(value);
if (exists && value !== row.username) {
return 'Username already taken';
}
return undefined;
},
},
}{
accessorKey: 'quantity',
header: 'Quantity',
meta: {
editable: true,
editType: 'number',
validate: (value) => {
if (value === undefined || value === null) {
return 'Quantity is required';
}
if (value < 0) {
return 'Must be positive';
}
if (value > 1000) {
return 'Maximum is 1000';
}
if (!Number.isInteger(value)) {
return 'Must be a whole number';
}
return undefined;
},
},
}Handle data changes with the onDataCommit callback:
interface EditingCommitPayload<TData> {
rowId: string;
rowIndex: number;
oldData: TData;
newData: TData;
changes: Partial<TData>;
}
<ActiveGrid
editMode="cell"
onDataCommit={async (payload) => {
console.log('Row ID:', payload.rowId);
console.log('Old data:', payload.oldData);
console.log('New data:', payload.newData);
console.log('Changes:', payload.changes);
// Save to server
await api.updateUser(payload.rowId, payload.changes);
}}
{...}
/><ActiveGrid
editMode="cell"
onDataCommit={async (payload) => {
try {
await api.updateUser(payload.rowId, payload.changes);
toast.success('Updated successfully');
} catch (error) {
toast.error('Failed to update');
throw error; // Reverts the change
}
}}
{...}
/>Create custom cell editors for specialized input:
Access edit state programmatically:
For server-side mode, handle updates appropriately:
Update UI immediately while saving in background:
const columns: GridColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
meta: {
editable: true,
editType: 'text',
validate: (value) => {
if (!value) return 'Name is required';
if (value.length < 2) return 'Minimum 2 characters';
return undefined;
},
},
},
{
accessorKey: 'email',
header: 'Email',
meta: {
editable: true,
editType: 'text',
validate: (value) => {
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Invalid email';
}
return undefined;
},
},
},
{
accessorKey: 'age',
header: 'Age',
meta: {
editable: true,
editType: 'number',
validate: (value) => {
if (value < 18) return 'Must be 18 or older';
if (value > 120) return 'Invalid age';
return undefined;
},
},
},
];
<ActiveGrid
editMode="cell"
columns={columns}
data={users}
onDataCommit={async (payload) => {
await updateUser(payload.rowId, payload.changes);
}}
/><ActiveGrid
editMode="row"
columns={columns}
data={data}
onDataCommit={async (payload) => {
// All changes in the row are committed at once
console.log('All changes:', payload.changes);
await api.updateRow(payload.rowId, payload.newData);
}}
/>{
accessorKey: 'tags',
header: 'Tags',
meta: {
editable: true,
},
cell: ({ getValue, row, column, table }) => {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(getValue());
if (isEditing) {
return (
<TagInput
value={value}
onChange={setValue}
onBlur={() => {
setIsEditing(false);
// Commit changes
}}
/>
);
}
return (
<div onClick={() => setIsEditing(true)}>
{value.map(tag => <Badge key={tag}>{tag}</Badge>)}
</div>
);
},
}import { useActiveGrid } from '@workspace/active-grid';
function MyComponent() {
const table = useActiveGrid();
const editState = table.options.meta?.editState;
const isEditing = editState !== null;
const editingRowIndex = editState?.rowIndex;
const editingColumnId = editState?.type === 'cell' ? editState.columnId : null;
}<ActiveGrid
mode="server"
fetchFn={fetchData}
editMode="cell"
onDataCommit={async (payload) => {
// Update server
await api.updateUser(payload.rowId, payload.changes);
// Refresh data to get server state
table.refetch();
}}
{...}
/><ActiveGrid
editMode="cell"
onDataCommit={async (payload) => {
// UI already updated optimistically
try {
await api.updateUser(payload.rowId, payload.changes);
} catch (error) {
// Revert on error
toast.error('Failed to save, reverting changes');
table.refetch();
throw error;
}
}}
{...}
/>type Product = {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
};
const columns: GridColumnDef<Product>[] = [
{
accessorKey: 'name',
header: 'Product',
meta: {
editable: true,
editType: 'text',
validate: (value) => {
if (!value) return 'Name is required';
if (value.length < 3) return 'Minimum 3 characters';
return undefined;
},
},
},
{
accessorKey: 'price',
header: 'Price',
meta: {
editable: true,
editType: 'number',
valueFormatter: (value) => `$${value.toFixed(2)}`,
validate: (value) => {
if (value < 0) return 'Price must be positive';
if (value > 10000) return 'Maximum price is $10,000';
return undefined;
},
},
},
{
accessorKey: 'category',
header: 'Category',
meta: {
editable: true,
editType: 'select',
editOptions: [
{ label: 'Electronics', value: 'electronics' },
{ label: 'Clothing', value: 'clothing' },
{ label: 'Books', value: 'books' },
],
},
},
{
accessorKey: 'inStock',
header: 'In Stock',
meta: {
editable: true,
editType: 'boolean',
},
},
];
function ProductsTable() {
const handleCommit = async (payload: EditingCommitPayload<Product>) => {
try {
await api.updateProduct(payload.rowId, payload.changes);
toast.success('Product updated');
} catch (error) {
toast.error('Failed to update product');
throw error;
}
};
return (
<ActiveGrid
columns={columns}
data={products}
editMode="cell"
onDataCommit={handleCommit}
/>
);
}import { EditingCommitPayload } from '@workspace/active-grid';
type User = {
id: string;
name: string;
email: string;
};
const handleCommit = async (payload: EditingCommitPayload<User>) => {
// payload.oldData: User
// payload.newData: User
// payload.changes: Partial<User>
await updateUser(payload.rowId, payload.changes);
};
<ActiveGrid<User>
editMode="cell"
columns={columns}
data={users}
onDataCommit={handleCommit}
/>