diff --git a/app/config.ts b/app/config.ts index 136787a..1e866f9 100644 --- a/app/config.ts +++ b/app/config.ts @@ -182,6 +182,11 @@ export const ROUTES: RouteInfo[] = [ title: 'Handling cell clicks', description: `Example: handling cell click events on ${PRODUCT_NAME}`, }, + { + href: '/examples/editing-cells', + title: 'Editing cells', + description: `Example: how to implement inline cell editing with ${PRODUCT_NAME}`, + }, { href: '/examples/using-with-mantine-contextmenu', title: `Using with ${MANTINE_CONTEXTMENU_PRODUCT_NAME}`, diff --git a/app/examples/editing-cells/EditingCellsCallbackExample.tsx b/app/examples/editing-cells/EditingCellsCallbackExample.tsx new file mode 100644 index 0000000..4b11af7 --- /dev/null +++ b/app/examples/editing-cells/EditingCellsCallbackExample.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { DataTable } from '__PACKAGE__'; +import { Code, Stack, Text } from '@mantine/core'; +import { useState } from 'react'; +import { type Company, companies } from '~/data'; + +const initialRecords = companies.slice(0, 3); + +type EditEvent = { accessor: string; value: string; recordId: number }; + +export function EditingCellsCallbackExample() { + const [records, setRecords] = useState(initialRecords); + const [log, setLog] = useState([]); + + return ( + // example-start + + { + setRecords((current) => + current.map((r) => (r.id === record.id ? { ...r, [accessor as keyof Company]: value } : r)) + ); + setLog((current) => [{ accessor: String(accessor), value, recordId: record.id }, ...current].slice(0, 5)); + }} + /> + {log.length > 0 && ( + + + Last commits (newest first): + + {log.map((entry, i) => ( + + {`{ accessor: "${entry.accessor}", value: "${entry.value}", record.id: ${entry.recordId} }`} + + ))} + + )} + + // example-end + ); +} diff --git a/app/examples/editing-cells/EditingCellsCustomRenderExample.tsx b/app/examples/editing-cells/EditingCellsCustomRenderExample.tsx new file mode 100644 index 0000000..f16fa8f --- /dev/null +++ b/app/examples/editing-cells/EditingCellsCustomRenderExample.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { DataTable } from '__PACKAGE__'; +import { Select } from '@mantine/core'; +import { useState } from 'react'; +import { type Company, companies } from '~/data'; + +const initialRecords = companies.slice(0, 5); + +const US_STATES = [ + 'AK', + 'AL', + 'AR', + 'AZ', + 'CA', + 'CO', + 'CT', + 'DC', + 'DE', + 'FL', + 'GA', + 'HI', + 'IA', + 'ID', + 'IL', + 'IN', + 'KS', + 'KY', + 'LA', + 'MA', + 'MD', + 'ME', + 'MI', + 'MN', + 'MO', + 'MS', + 'MT', + 'NC', + 'ND', + 'NE', + 'NH', + 'NJ', + 'NM', + 'NV', + 'NY', + 'OH', + 'OK', + 'OR', + 'PA', + 'RI', + 'SC', + 'SD', + 'TN', + 'TX', + 'UT', + 'VA', + 'VT', + 'WA', + 'WI', + 'WV', + 'WY', +]; + +export function EditingCellsCustomRenderExample() { + const [records, setRecords] = useState(initialRecords); + + return ( + // example-start + ( + onEditChange(e.target.value)} + onFocus={onCellFocus} + onKeyDown={(e) => { + if (commitEditOn.includes('enter') && e.key === 'Enter') { + committingRef.current = true; + onEditCommit(); + } + if (e.key === 'Escape') onEditCancel(); + if (e.key === 'Tab' && onFocusNextCell) { + e.preventDefault(); + committingRef.current = true; + onEditCommit(); + onFocusNextCell(e.shiftKey ? 'prev' : 'next'); + } + }} + onBlur={() => { + if (committingRef.current) { + committingRef.current = false; + onCellBlur?.(); + return; + } + onCellBlur?.(); + if (commitEditOn.includes('blur')) onEditCommit(); + else onEditCancel(); + }} + style={{ + background: 'none', + border: 'none', + outline: 'none', + padding: 0, + width: '100%', + font: 'inherit', + color: 'inherit', + }} + /> + ) + ) : render ? ( + render(record, index) + ) : defaultRender ? ( + defaultRender(record, index, accessor) + ) : ( + (getValueAtPath(record, accessor) as React.ReactNode) + )} ); } diff --git a/package/types/DataTableColumn.ts b/package/types/DataTableColumn.ts index ef65a2a..b7ac039 100644 --- a/package/types/DataTableColumn.ts +++ b/package/types/DataTableColumn.ts @@ -1,5 +1,6 @@ import type { MantineStyleProp, MantineTheme, PopoverProps } from '@mantine/core'; import type { DataTableColumnTextAlign } from './DataTableColumnTextAlign'; +import type { DataTableEditRenderContext } from './DataTableEditRenderContext'; export type DataTableColumn> = { /** @@ -179,6 +180,27 @@ export type DataTableColumn> = { * Optional style passed to the column footer. */ footerStyle?: MantineStyleProp; + + /** + * If true, cells in this column are editable on double-click. + * Can be a function receiving the current record and its index, returning a boolean, + * allowing per-cell control over editability. + */ + editable?: boolean | ((record: T, index: number) => boolean); + + /** + * Custom edit UI for this column. + * If provided, replaces the built-in unstyled TextInput when a cell enters edit mode. + * Receives the current record, its index, and a context with value/onChange/onCommit/onCancel. + */ + editRender?: (record: T, index: number, context: DataTableEditRenderContext) => React.ReactNode; + + /** + * Events that commit the edit for cells in this column. + * Overrides `defaultCommitEditOn` set at the DataTable level. + * @default ['blur', 'enter'] + */ + commitEditOn?: ('blur' | 'enter')[]; } & ( | { /** diff --git a/package/types/DataTableEditRenderContext.ts b/package/types/DataTableEditRenderContext.ts new file mode 100644 index 0000000..35be566 --- /dev/null +++ b/package/types/DataTableEditRenderContext.ts @@ -0,0 +1,8 @@ +export type DataTableEditRenderContext = { + value: string; + onChange: (value: string) => void; + /** Commit the edit. Pass an optional value to override the tracked input value + * (useful for custom controls like Select that change + commit in one event). */ + onCommit: (value?: string) => void; + onCancel: () => void; +}; diff --git a/package/types/DataTableProps.ts b/package/types/DataTableProps.ts index b9dfab4..f52f2e9 100644 --- a/package/types/DataTableProps.ts +++ b/package/types/DataTableProps.ts @@ -163,6 +163,42 @@ export type DataTableProps> = { */ onCellContextMenu?: DataTableCellClickHandler; + /** + * Callback fired when an inline cell edit is committed. + * Receives the updated record, its index, the column accessor, and the new string value. + */ + onCellEdit?: (params: { + record: T; + index: number; + accessor: keyof T | (string & NonNullable); + value: string; + }) => void; + + /** + * Default commit trigger events for all editable columns. + * Can be overridden per column via `commitEditOn`. + * @default ['blur', 'enter'] + */ + defaultCommitEditOn?: ('blur' | 'enter')[]; + + /** + * Edit mode for editable cells. + * - `'cell'` (default): double-click a cell to enter edit mode one at a time. + * - `'global'`: all editable cells are in edit mode simultaneously. + * @default 'cell' + */ + editMode?: 'cell' | 'global'; + + /** + * Style applied to the `` of the currently focused editing cell. + */ + editingCellStyle?: MantineStyleProp; + + /** + * Class name applied to the `` of the currently focused editing cell. + */ + editingCellClassName?: string; + /** * Function to call when a row is clicked. * Receives an object with the current record, its index in `records` and the click event diff --git a/package/types/index.ts b/package/types/index.ts index 3a4154c..1236312 100644 --- a/package/types/index.ts +++ b/package/types/index.ts @@ -4,6 +4,7 @@ export * from './DataTableColumnGroup'; export * from './DataTableColumnTextAlign'; export * from './DataTableDefaultColumnProps'; export * from './DataTableDraggableRowProps'; +export * from './DataTableEditRenderContext'; export * from './DataTableEmptyStateProps'; export * from './DataTableOuterBorderProps'; export * from './DataTablePaginationProps';