Skip to content
Open
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
5 changes: 5 additions & 0 deletions app/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
32 changes: 32 additions & 0 deletions app/examples/inline-editing/InlineEditingExample.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DataTable
columns={[
{
accessor: 'name',
editable: true,

onEdit: (record, index) => {
const newData = [...data];
newData[index] = record;
setData(newData);
},
},
{ accessor: 'streetAddress' },
{ accessor: 'city' },
{ accessor: 'state' },
]}
records={data}
/>
);
}
39 changes: 39 additions & 0 deletions app/examples/inline-editing/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string>(`${PATH}/InlineEditingExample.tsx`),
'companies.json': readCodeFile<string>('/../data/companies.json'),
});

return (
<>
<PageTitle of={PATH} />
<Txt>
This example demonstrates how to implement inline cell editing in {PRODUCT_NAME}.
This is achieved by setting the <code>editable</code> property to <code>true</code> in the column definition.
Additionally, the <code>onEdit</code> callback is provided to handle updates to the record when the cell value is changed.
In this example, we allow editing of the <code>name</code> field of company records.

This is baked in to the <code>DataTable</code> 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 <code>render</code> function of the column to show input fields when in edit mode.
</Txt>
<CodeBlock tabs={{ code, keys: ['InlineEditingExample.tsx', 'companies.json'] }} />
<InlineEditingExample />
<PageNavigation of={PATH} />
</>
);
}
4 changes: 4 additions & 0 deletions package/DataTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export function DataTableRow<T>({
cellsClassName,
cellsStyle,
customCellAttributes,
editable,
onEdit,
} = { ...defaultColumnProps, ...columnProps };

return (
Expand Down Expand Up @@ -148,6 +150,8 @@ export function DataTableRow<T>({
render={render}
defaultRender={defaultColumnRender}
customCellAttributes={customCellAttributes}
editable={editable}
onEdit={onEdit}
/>
);
})}
Expand Down
77 changes: 66 additions & 11 deletions package/DataTableRowCell.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -19,14 +20,23 @@ type DataTableRowCellProps<T> = {
record: T;
index: number;
defaultRender:
| ((record: T, index: number, accessor: keyof T | (string & NonNullable<unknown>)) => React.ReactNode)
| undefined;
| ((record: T, index: number, accessor: keyof T | (string & NonNullable<unknown>)) => React.ReactNode)
| undefined;
onClick: React.MouseEventHandler<HTMLTableCellElement> | undefined;
onDoubleClick: React.MouseEventHandler<HTMLTableCellElement> | undefined;
onContextMenu: React.MouseEventHandler<HTMLTableCellElement> | undefined;
} & Pick<
DataTableColumn<T>,
'accessor' | 'visibleMediaQuery' | 'textAlign' | 'width' | 'noWrap' | 'ellipsis' | 'render' | 'customCellAttributes'
| 'accessor'
| 'visibleMediaQuery'
| 'textAlign'
| 'width'
| 'noWrap'
| 'ellipsis'
| 'render'
| 'customCellAttributes'
| 'editable'
| 'onEdit'
>;

export function DataTableRowCell<T>({
Expand All @@ -46,15 +56,46 @@ export function DataTableRowCell<T>({
render,
defaultRender,
customCellAttributes,
editable,
onEdit,
}: DataTableRowCellProps<T>) {
const [isEditing, setIsEditing] = useState(false);
const [editedValue, setEditedValue] = useState<string>('');

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<HTMLTableCellElement>) => {
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 (
<TableTd
className={clsx(
{
[NOWRAP]: noWrap || ellipsis,
[ELLIPSIS]: ellipsis,
[POINTER_CURSOR]: onClick || onDoubleClick,
[POINTER_CURSOR]: (onClick || onDoubleClick || editable) && !isEditing,
[CONTEXT_MENU_CURSOR]: onContextMenu,
[TEXT_ALIGN_LEFT]: textAlign === 'left',
[TEXT_ALIGN_CENTER]: textAlign === 'center',
Expand All @@ -70,16 +111,30 @@ export function DataTableRowCell<T>({
},
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 ? (
<TextInput
value={editedValue}
onChange={(event) => setEditedValue(event.currentTarget.value)}
onBlur={handleEdit}
onKeyDown={(event) => {
if (event.key === 'Enter') {
handleEdit();
}
if (event.key === 'Escape') {
setIsEditing(false);
}
}}
autoFocus

/>
) : (
cellContent
)}
</TableTd>
);
}
25 changes: 22 additions & 3 deletions package/types/DataTableColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,26 @@ export type DataTableColumn<T = Record<string, unknown>> = {
*/
footerStyle?: MantineStyleProp;
} & (
| {
| {
/**
* If true, the cells in this column will be editable.
*/
editable?: false;
onEdit?: 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;
}
) & (
| {
/**
* 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`).
Expand All @@ -177,7 +196,7 @@ export type DataTableColumn<T = Record<string, unknown>> = {

noWrap?: never;
}
| {
| {
ellipsis?: never;

/**
Expand All @@ -187,4 +206,4 @@ export type DataTableColumn<T = Record<string, unknown>> = {
*/
noWrap?: boolean;
}
);
);