From 683b2cae6a186102c56ce6546c5b24909b08000c Mon Sep 17 00:00:00 2001 From: mikku Date: Wed, 12 Nov 2025 23:12:38 +0200 Subject: [PATCH 1/2] feat: Rudementary inline cell editing as a library function Implement simple cell editing that edits the record from the datatable data in place. Provide the onEdit callback to allow hooking external logic to the featureset. This is mostly a small version of the grander feature as has been discussed at length in some issues where a workaround was suggested to use the render function of the column. However, this appears to be very cumbersome to do and maintain. In this case, I belive that I speak for others too, that this may be a good core feature of the datatable with room for expansion in the future. --- app/config.ts | 5 ++ .../inline-editing/InlineEditingExample.tsx | 32 ++++++++ app/examples/inline-editing/page.tsx | 39 ++++++++++ package/DataTableRow.tsx | 4 + package/DataTableRowCell.tsx | 77 ++++++++++++++++--- package/types/DataTableColumn.ts | 26 ++++++- 6 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 app/examples/inline-editing/InlineEditingExample.tsx create mode 100644 app/examples/inline-editing/page.tsx diff --git a/app/config.ts b/app/config.ts index 995e6cba9..eaf5921f8 100644 --- a/app/config.ts +++ b/app/config.ts @@ -252,6 +252,11 @@ export const ROUTES: RouteInfo[] = [ title: 'Complex usage scenario', description: `Example: a complex usage scenario for ${PRODUCT_NAME} featuring custom column definitions, asynchronous data loading with React Query, sorting, pagination, custom cell rendering, multiple row selection, and more`, }, + { + href: '/examples/inline-editing', + title: 'Inline editing', + description: `Example: inline editing with ${PRODUCT_NAME}`, + }, { href: '/type-definitions', title: 'Type definitions', diff --git a/app/examples/inline-editing/InlineEditingExample.tsx b/app/examples/inline-editing/InlineEditingExample.tsx new file mode 100644 index 000000000..19d3a2b03 --- /dev/null +++ b/app/examples/inline-editing/InlineEditingExample.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { DataTable } from '__PACKAGE__'; +import { useState } from 'react'; +import companies from '~/data/companies.json'; + +const records = companies.slice(0, 5); + +export function InlineEditingExample() { + const [data, setData] = useState(records); + + return ( + { + const newData = [...data]; + newData[index] = record; + setData(newData); + }, + }, + { accessor: 'streetAddress' }, + { accessor: 'city' }, + { accessor: 'state' }, + ]} + records={data} + /> + ); +} diff --git a/app/examples/inline-editing/page.tsx b/app/examples/inline-editing/page.tsx new file mode 100644 index 000000000..04522ab6c --- /dev/null +++ b/app/examples/inline-editing/page.tsx @@ -0,0 +1,39 @@ +import type { Route } from 'next'; +import { PRODUCT_NAME } from '~/app/config'; +import { CodeBlock } from '~/components/CodeBlock'; +import { PageNavigation } from '~/components/PageNavigation'; +import { PageTitle } from '~/components/PageTitle'; +import { Txt } from '~/components/Txt'; +import { readCodeFile } from '~/lib/code'; +import { allPromiseProps, getRouteMetadata } from '~/lib/utils'; +import { InlineEditingExample } from './InlineEditingExample'; + +const PATH: Route = '/examples/inline-editing'; + +export const metadata = getRouteMetadata(PATH); + +export default async function InlineEditingExamplePage() { + const code = await allPromiseProps({ + 'InlineEditingExample.tsx': readCodeFile(`${PATH}/InlineEditingExample.tsx`), + 'companies.json': readCodeFile('/../data/companies.json'), + }); + + return ( + <> + + + This example demonstrates how to implement inline cell editing in {PRODUCT_NAME}. + This is achieved by setting the editable property to true in the column definition. + Additionally, the onEdit callback is provided to handle updates to the record when the cell value is changed. + In this example, we allow editing of the name field of company records. + + This is baked in to the DataTable component for the column definitions, so no additional libraries are required. However, + this only supports the basic single cell editing scenario, for a more complex case of editing the entire row or adding validation it is still recommended to + implement the logic yourself by changing the logic of the render function of the column to show input fields when in edit mode. + + + + + + ); +} diff --git a/package/DataTableRow.tsx b/package/DataTableRow.tsx index 596e454eb..11d50ed12 100644 --- a/package/DataTableRow.tsx +++ b/package/DataTableRow.tsx @@ -115,6 +115,8 @@ export function DataTableRow({ cellsClassName, cellsStyle, customCellAttributes, + editable, + onEdit, } = { ...defaultColumnProps, ...columnProps }; return ( @@ -148,6 +150,8 @@ export function DataTableRow({ render={render} defaultRender={defaultColumnRender} customCellAttributes={customCellAttributes} + editable={editable} + onEdit={onEdit} /> ); })} diff --git a/package/DataTableRowCell.tsx b/package/DataTableRowCell.tsx index d56168467..0db26281e 100644 --- a/package/DataTableRowCell.tsx +++ b/package/DataTableRowCell.tsx @@ -1,5 +1,6 @@ -import { TableTd, type MantineStyleProp } from '@mantine/core'; +import { TableTd, TextInput, type MantineStyleProp } from '@mantine/core'; import clsx from 'clsx'; +import { useState } from 'react'; import { useMediaQueryStringOrFunction } from './hooks'; import type { DataTableColumn } from './types'; import { @@ -19,14 +20,23 @@ type DataTableRowCellProps = { record: T; index: number; defaultRender: - | ((record: T, index: number, accessor: keyof T | (string & NonNullable)) => React.ReactNode) - | undefined; + | ((record: T, index: number, accessor: keyof T | (string & NonNullable)) => React.ReactNode) + | undefined; onClick: React.MouseEventHandler | undefined; onDoubleClick: React.MouseEventHandler | undefined; onContextMenu: React.MouseEventHandler | undefined; } & Pick< DataTableColumn, - 'accessor' | 'visibleMediaQuery' | 'textAlign' | 'width' | 'noWrap' | 'ellipsis' | 'render' | 'customCellAttributes' + | 'accessor' + | 'visibleMediaQuery' + | 'textAlign' + | 'width' + | 'noWrap' + | 'ellipsis' + | 'render' + | 'customCellAttributes' + | 'editable' + | 'onEdit' >; export function DataTableRowCell({ @@ -46,15 +56,46 @@ export function DataTableRowCell({ render, defaultRender, customCellAttributes, + editable, + onEdit, }: DataTableRowCellProps) { + const [isEditing, setIsEditing] = useState(false); + const [editedValue, setEditedValue] = useState(''); + if (!useMediaQueryStringOrFunction(visibleMediaQuery)) return null; + + const handleEdit = () => { + if (onEdit) { + const newRecord = { ...record, [accessor as keyof T]: editedValue }; + onEdit(newRecord, index); + } + setIsEditing(false); + }; + + const handleClick = (e: React.MouseEvent) => { + if (editable) { + setIsEditing(true); + setEditedValue(getValueAtPath(record, accessor) as string); + } + onClick?.(e); + }; + + let cellContent: React.ReactNode; + if (render) { + cellContent = render(record, index); + } else if (defaultRender) { + cellContent = defaultRender(record, index, accessor); + } else { + cellContent = getValueAtPath(record, accessor) as React.ReactNode; + } + return ( ({ }, style, ]} - onClick={onClick} + onClick={handleClick} onDoubleClick={onDoubleClick} onContextMenu={onContextMenu} {...customCellAttributes?.(record, index)} > - {render - ? render(record, index) - : defaultRender - ? defaultRender(record, index, accessor) - : (getValueAtPath(record, accessor) as React.ReactNode)} + {isEditing ? ( + setEditedValue(event.currentTarget.value)} + onBlur={handleEdit} + onKeyDown={(event) => { + if (event.key === 'Enter') { + handleEdit(); + } + if (event.key === 'Escape') { + setIsEditing(false); + } + }} + autoFocus + + /> + ) : ( + cellContent + )} ); } diff --git a/package/types/DataTableColumn.ts b/package/types/DataTableColumn.ts index ff3d32b04..3d8edc1fd 100644 --- a/package/types/DataTableColumn.ts +++ b/package/types/DataTableColumn.ts @@ -1,4 +1,4 @@ -import type { MantineStyleProp, MantineTheme, PopoverProps } from '@mantine/core'; +import type { MantineStyleProp, MantineTheme, PopoverProps, TextInputProps } from '@mantine/core'; import type { DataTableColumnTextAlign } from './DataTableColumnTextAlign'; export type DataTableColumn> = { @@ -167,6 +167,30 @@ export type DataTableColumn> = { */ footerStyle?: MantineStyleProp; } & ( + | { + /** + * If true, the cells in this column will be editable. + */ + editable?: false; + onEdit?: never; + textInputProps?: never; + } + | { + /** + * If true, the cells in this column will be editable. + */ + editable: true; + /** + * Callback fired when a cell in this column is edited. + * Receives the edited record and its index as arguments. + */ + onEdit: (record: T, index: number) => void; + /** + * Optional props to pass to the TextInput component when a cell is editable. + */ + textInputProps?: Omit; + } +) & ( | { /** * If true, cell content in this column will be truncated with ellipsis as needed and will not wrap From 873b5aeaaae4b7943f3f514b030c313817fcb583 Mon Sep 17 00:00:00 2001 From: mikku Date: Wed, 12 Nov 2025 23:19:39 +0200 Subject: [PATCH 2/2] Remove unused prop --- package/types/DataTableColumn.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/package/types/DataTableColumn.ts b/package/types/DataTableColumn.ts index 3d8edc1fd..a1a0f88c1 100644 --- a/package/types/DataTableColumn.ts +++ b/package/types/DataTableColumn.ts @@ -1,4 +1,4 @@ -import type { MantineStyleProp, MantineTheme, PopoverProps, TextInputProps } from '@mantine/core'; +import type { MantineStyleProp, MantineTheme, PopoverProps } from '@mantine/core'; import type { DataTableColumnTextAlign } from './DataTableColumnTextAlign'; export type DataTableColumn> = { @@ -167,15 +167,14 @@ export type DataTableColumn> = { */ footerStyle?: MantineStyleProp; } & ( - | { + | { /** * If true, the cells in this column will be editable. */ editable?: false; onEdit?: never; - textInputProps?: never; } - | { + | { /** * If true, the cells in this column will be editable. */ @@ -185,13 +184,9 @@ export type DataTableColumn> = { * Receives the edited record and its index as arguments. */ onEdit: (record: T, index: number) => void; - /** - * Optional props to pass to the TextInput component when a cell is editable. - */ - textInputProps?: Omit; } -) & ( - | { + ) & ( + | { /** * If true, cell content in this column will be truncated with ellipsis as needed and will not wrap * to multiple lines (i.e. `overflow: hidden; text-overflow: ellipsis`; `white-space: nowrap`). @@ -201,7 +196,7 @@ export type DataTableColumn> = { noWrap?: never; } - | { + | { ellipsis?: never; /** @@ -211,4 +206,4 @@ export type DataTableColumn> = { */ noWrap?: boolean; } -); + );