Cell Editing
Learn the core cell-editing flow, from declaring editable cells and entering by pointer or keyboard to IME input, save, cancel, and movement.
import * as React from 'react';
import { BGrid, type BGridCellAddress, type BGridColumn } from 'beautiful-grid';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import {
applyEditingDataChange,
cloneEditingOrders,
type EditingOrder,
withEditingCellClasses,
} from './editing/shared';
export default function BasicEditingExample() {
const [data, setData] = React.useState(cloneEditingOrders);
const [activeCell, setActiveCell] = React.useState<BGridCellAddress>({ rowIndex: 0, columnIndex: 1 });
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const columns = React.useMemo<BGridColumn<EditingOrder>[]>(
() => withEditingCellClasses<EditingOrder>([
{ key: 'orderCode', label: '주문 코드', width: 150, editable: false },
{
key: 'customerName',
label: '고객명 · 더블클릭',
width: 190,
editable: true,
editor: { type: 'text', inputProps: { maxLength: 50, autoComplete: 'off' } },
},
{
key: 'note',
label: '메모 · 한 번 클릭',
width: 210,
editable: true,
editTrigger: 'click',
editor: { type: 'text', inputProps: { maxLength: 80, autoComplete: 'off' } },
},
{ key: 'status', label: '상태 · 읽기 전용', width: 130, editable: false },
]),
[],
);
return (
<div className='flex min-h-0 flex-col gap-3'>
<div className='rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm leading-6 text-slate-700'>
<strong>마우스:</strong> 고객명은 Grid 기본값인 더블클릭, 메모는 컬럼의 <code>editTrigger='click'</code>으로
편집합니다.
<br />
<strong>키보드:</strong> 방향키로 셀을 이동하고 바로 입력하면 기존 값을 대체합니다. <kbd>Enter</kbd> 또는{' '}
<kbd>F2</kbd>는 기존 값을 유지하며 시작하고, <kbd>Tab</kbd>은 저장 후 이동, <kbd>Escape</kbd>는 취소합니다.
<output aria-live='polite' className='mt-1 block font-mono text-xs text-blue-700'>
활성 셀: 행 {activeCell.rowIndex + 1}, 열 {activeCell.columnIndex + 1}
</output>
</div>
<DataGridContainer ref={containerRef} style={{ height: 340 }}>
<BGrid<EditingOrder>
width={width}
height={height}
data={data}
columns={columns}
rowKey='id'
editable
variant='vertical-bordered'
editTrigger='dblclick'
showLineNumber
cellSelectionOptions={{ enabled: true }}
cellNavigationOptions={{
enabled: true,
editOnEnter: true,
activeCell,
onActiveCellChange: cell => cell && setActiveCell(cell),
}}
onChangeData={(sourceIndex, _columnIndex, values, _column, meta) => {
setData(current => applyEditingDataChange(current, sourceIndex, values, meta));
}}
/>
</DataGridContainer>
</div>
);
}import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* BGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .bgrid-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}import type { BGridChangeDataMeta, BGridColumn, BGridDataItem } from 'beautiful-grid';
import './editingExamples.css';
export interface EditingOrder {
id: string;
orderCode: string;
customerCode: string;
customerName: string;
customerGrade: '일반' | '우수' | 'VIP';
status: '접수' | '진행' | '완료';
deliveryDate: string;
quantity: number;
unitPrice: number;
amount: number;
note: string;
mergeGroup: string;
}
export const editingOrders: BGridDataItem<EditingOrder>[] = [
{
values: {
id: 'ORDER-001',
orderCode: 'ORD-2601',
customerCode: 'C001',
customerName: '서울상사',
customerGrade: 'VIP',
status: '접수',
deliveryDate: '2026-08-25',
quantity: 2,
unitPrice: 12000,
amount: 24000,
note: '오전 배송',
mergeGroup: 'A',
},
},
{
values: {
id: 'ORDER-002',
orderCode: 'ORD-2602',
customerCode: 'C001',
customerName: '서울상사',
customerGrade: 'VIP',
status: '진행',
deliveryDate: '2026-08-26',
quantity: 3,
unitPrice: 18000,
amount: 54000,
note: '담당자 확인',
mergeGroup: 'A',
},
},
{
values: {
id: 'ORDER-003',
orderCode: 'ORD-2603',
customerCode: 'C002',
customerName: '한빛물산',
customerGrade: '우수',
status: '완료',
deliveryDate: '2026-08-28',
quantity: 1,
unitPrice: 32000,
amount: 32000,
note: '',
mergeGroup: 'B',
},
},
{
values: {
id: 'ORDER-004',
orderCode: 'ORD-2604',
customerCode: 'C003',
customerName: 'Northwind',
customerGrade: '일반',
status: '접수',
deliveryDate: '2026-09-01',
quantity: 5,
unitPrice: 9000,
amount: 45000,
note: '영문 송장',
mergeGroup: 'C',
},
},
];
export const cloneEditingOrders = () =>
editingOrders.map(item => ({
...item,
values: { ...item.values },
editedColumnIds: item.editedColumnIds ? [...item.editedColumnIds] : undefined,
changedKeys: item.changedKeys ? [...item.changedKeys] : undefined,
}));
export const applyEditingDataChange = <T,>(
current: BGridDataItem<T>[],
sourceIndex: number,
values: T,
meta?: BGridChangeDataMeta<T>,
): BGridDataItem<T>[] =>
current.map((item, index) =>
index === sourceIndex ? meta?.dataItem ?? { ...item, values } : item,
);
export const withEditingCellClasses = <T,>(columns: BGridColumn<T>[]): BGridColumn<T>[] =>
columns.map(column => ({
...column,
className: [
column.className,
column.editable === false ? 'editing-example-cell-readonly' : 'editing-example-cell-editable',
]
.filter(Boolean)
.join(' '),
}));.editing-example-cell-editable {
--editing-example-bg: #ffffff;
--editing-example-hover-bg: #dbeafe;
}
.editing-example-cell-readonly {
--editing-example-color: #525252;
--editing-example-bg: #f5f5f5;
--editing-example-hover-bg: #e5e5e5;
}
.bgrid-body-table
td:is(.editing-example-cell-editable, .editing-example-cell-readonly):not(:is(.bgrid-cell-selected, .bgrid-cell-edited, .bgrid-cell-value-changed, .bgrid-cell-editing)) {
color: var(--editing-example-color, inherit);
background-color: var(--editing-example-bg);
}
.bgrid-body-table tr.bgrid-row-hover
> td:is(.editing-example-cell-editable, .editing-example-cell-readonly):not(:is(.bgrid-cell-selected, .bgrid-cell-edited, .bgrid-cell-value-changed, .bgrid-cell-editing)) {
background-color: var(--editing-example-hover-bg);
}To enable cell editing, set editable on both the Grid and the target column, then define column.editor. This page covers the core flow from pointer activation through keyboard and IME input, saving, canceling, and navigation. Separate guides cover Select, lookup, and editing-event extensions.
1. Minimum setup
const columns: BGridColumn<Order>[] = [
{
key: 'customerName',
label: 'Customer name',
width: 180,
editable: true,
editor: { type: 'text' },
},
];
<BGrid<Order>
width={720}
height={360}
data={data}
columns={columns}
rowKey='id'
editable
/>
itemRender controls how a cell appears when it is not being edited, while editor provides the input UI during editing. You do not need an editor merely to change display formatting.
2. Grid defaults and column overrides
The default edit trigger is a double-click. Change the Grid-wide default with editTrigger, then override individual columns that should open immediately, such as a Select editor.
<BGrid editTrigger='dblclick' {...props} />
const columns: BGridColumn<Order>[] = [
{ key: 'name', editable: true, editor: { type: 'text' } },
{
key: 'status',
editable: true,
editTrigger: 'click',
editor: statusEditor,
},
];
The resolution order is column.editTrigger → grid.editTrigger → 'dblclick'. There is no editTrigger: 'none'. Use editable: false to make a cell read-only. Put button actions that are independent of cell editing in editorIcon.onClick or itemRender.
3. Start editing with the pointer or keyboard
- Cell click or double-click: opens the editor according to the configured
editTrigger. - Direct character input: opens a text editor and replaces the existing value.
EnterorF2: opens the editor while preserving the existing value.- Icon click: opens the editor when
editorIcon.onClickis not defined.
Cell focus and editor focus are separate states. Activate a cell first, then open its editor from the keyboard. When editing ends, the Grid returns focus to the active cell.
4. Keyboard behavior
| Key | Cell focused | Editing |
|---|---|---|
| Character input | Starts a text edit by replacing the existing value | Normal text input |
Enter / F2 |
Starts editing while preserving the existing value | Enter saves |
Tab / Shift+Tab |
Moves to the next/previous cell | Saves, then moves to the next/previous cell |
Escape |
Clears the selection range | Cancels changes and returns to the same cell |
| Arrow keys | Moves the active cell | Uses the input control’s default behavior |
Ctrl/Cmd+C, V |
Copies or pastes the selected range | Uses the input control’s default behavior |
The built-in text editor defaults startOnInput to true. Set startOnInput: false to prevent direct character input on a focused cell from opening the editor.
editor: {
type: 'text',
startOnInput: false,
}
5. IME and keyboard movement settings
During IME composition, such as Korean text input, the Grid does not save an incomplete string even if Enter or blur occurs first. It commits the final string only after composition ends. For external plugins, also verify the composition behavior of the UI component you use.
<BGrid
cellNavigationOptions={{
enabled: true,
editOnEnter: true,
wrap: false,
}}
/>
To control the active cell or review Home/End and PageUp/PageDown behavior, continue to Cell Focus and Keyboard Navigation.
6. Update application state
After the Grid saves a change internally, it calls onChangeData. Its first argument is the source index in the original data, even when sorting or filtering is active.
onChangeData={(sourceIndex, _columnIndex, values, _column, meta) => {
setData(current =>
current.map((item, index) =>
index === sourceIndex ? meta?.dataItem ?? { ...item, values } : item,
),
);
}}
The actual row data always lives in BGridDataItem<T>.values. Saving meta.dataItem also preserves the editedColumnIds for directly edited columns and the changedKeys for changed data fields. Directly edited cells receive the bgrid-cell-edited style, while all cells that share a changed key receive bgrid-cell-value-changed.
7. Choose the next guide
| Goal | Next guide |
|---|---|
| Use text, Select, and Date editors | Built-in Editors |
| Integrate Ant Design or another external UI library | External Editor Plugins |
| Show dropdown or search icons in idle cells | Editor Icons |
| Combine autocomplete input with a lookup modal | Lookup Editor |
| Validate changes and update related cells | Editing Events and Transactions |
| Edit merged cells across frozen boundaries | Merged Cell Editing |