DataGrid
DataGrid - 편집형 데이터 입력
폼 테이블 형태의 즉시 편집 UX를 빠르게 구성하는 패턴입니다.
reopt design업데이트
1. 라이브 예제
스프레드시트 스타일로 즉시 편집 가능한 기본 데이터그리드. 셀 커밋 로그를 확인할 수 있습니다.
Last commit: -
Member 1
Owner
Seoul
Blocked
Member 2
Editor
Tokyo
Active
Member 3
Viewer
Singapore
Active
Member 4
Owner
Berlin
Active
Member 5
Editor
Seoul
Active
Member 6
Viewer
Tokyo
Blocked
Member 7
Owner
Singapore
Active
Member 8
Editor
Berlin
Active
Member 9
Viewer
Seoul
Active
Member 10
Owner
Tokyo
Active
Member 11
Editor
Singapore
Blocked
Member 12
Viewer
Berlin
Active
Member 13
Owner
Seoul
Active
Member 14
Editor
Tokyo
Active
Member 15
Viewer
Singapore
Active
Member 16
Owner
Berlin
Blocked
Member 17
Editor
Seoul
Active
Member 18
Viewer
Tokyo
Active
Member 19
Owner
Singapore
Active
Member 20
Editor
Berlin
Active
2. 설계 포인트
- 편집 가능한 컬럼만 `editable: true`로 제한합니다.
- `setValue`는 불변 업데이트를 반환해 상태 추적을 안정화합니다.
- 저장 전 검증이 필요하면 `onRowsChange` 단계에서 규칙을 적용합니다.
- 커스텀 편집기는 editor.kind = "custom"과 component 조합을 기본 경로로 사용합니다.
3. 커스텀 에디터 API
재사용 가능한 편집기는 column definition 안에서 kind: "custom"과 component를 함께 선언하는 방식이 기본입니다.
커스텀 편집기 로직은 component 안에서 `commit`, `cancel`, `setDraftValue`를 직접 호출하는 방식으로 마무리하는 편이 가장 예측 가능하고 재사용하기 쉽습니다.
tsx
import * as React from "react";
import {
DataGrid,
type DataGridColumn,
type DataGridEditorContext,
} from "@reopt-ai/opt-datagrid";
interface MemberRow {
id: number;
status: "pending" | "active";
}
function StatusEditor(
context: DataGridEditorContext<MemberRow, MemberRow["status"]>,
) {
const { draftValue, setDraftValue, commit, cancel } = context;
return (
<div className="grid gap-2">
<p className="text-xs text-zinc-500">Current: {draftValue}</p>
<div className="flex gap-2">
<button
type="button"
onClick={() => {
setDraftValue("pending");
commit();
}}
>
Pending
</button>
<button
type="button"
onClick={() => {
setDraftValue("active");
commit();
}}
>
Active
</button>
</div>
<button type="button" onClick={cancel}>
Cancel
</button>
</div>
);
}
const columns: DataGridColumn<MemberRow, MemberRow["status"]>[] = [
{
id: "status",
title: "Status",
editable: true,
editor: {
kind: "custom",
component: StatusEditor,
},
getValue: (row) => row.status,
setValue: (row, nextValue) => ({ ...row, status: nextValue }),
},
];
export function CustomEditorGrid() {
const [rows, setRows] = React.useState<MemberRow[]>([
{ id: 1, status: "pending" },
{ id: 2, status: "active" },
]);
return (
<DataGrid
rows={rows}
columns={columns}
onRowsChange={(nextRows) => setRows(Array.from(nextRows))}
height={320}
ariaLabel="Custom editor grid"
/>
);
}
// custom editor는 component 기반으로만 선언합니다.4. 인터셉트/검증 패턴
복수 셀 편집, 붙여넣기, 삭제는 `onCellsEdited`, `onPaste`, `onDelete` 에서 도메인 정책으로 제어할 수 있습니다.
tsx
import * as React from "react";
import {
DataGrid,
type DataGridCellEdit,
type DataGridColumn,
type DataGridSelection,
} from "@reopt-ai/opt-datagrid";
interface Row {
id: number;
name: string;
role: string;
}
const initialRows: Row[] = [
{ id: 1, name: "Mina", role: "Owner" },
{ id: 2, name: "Joon", role: "Editor" },
];
const columns: DataGridColumn<Row>[] = [
{
id: "name",
title: "Name",
editable: true,
getValue: (row) => row.name,
setValue: (row, nextValue) => ({ ...row, name: nextValue }),
},
{
id: "role",
title: "Role",
editable: true,
getValue: (row) => row.role,
setValue: (row, nextValue) => ({ ...row, role: nextValue }),
},
];
function EditableWithPolicy() {
const [rows, setRows] = React.useState(initialRows);
const handleCellsEdited = React.useCallback(
(edits: readonly DataGridCellEdit<Row>[]) => {
const hasBlockedField = edits.some(
(edit) => edit.column.id === "role" && edit.nextValue === "Owner",
);
if (hasBlockedField) return true; // 기본 적용 차단
return undefined; // 기본 적용 허용
},
[],
);
const handleDelete = React.useCallback((selection: DataGridSelection) => {
if (selection.start.row === 0) return true; // 첫 행 삭제 방지
return undefined;
}, []);
return (
<DataGrid
rows={rows}
columns={columns}
onRowsChange={setRows}
onCellsEdited={handleCellsEdited}
onDelete={handleDelete}
onPaste={() => undefined}
/>
);
}5. 코드 예제
tsx
import * as React from "react";
import { DataGrid, type DataGridColumn } from "@reopt-ai/opt-datagrid";
interface MemberRow {
id: number;
name: string;
role: string;
region: string;
}
const columns: DataGridColumn<MemberRow>[] = [
{
id: "name",
title: "Name",
width: 220,
editable: true,
getValue: (row) => row.name,
setValue: (row, nextValue) => ({ ...row, name: nextValue }),
},
{
id: "role",
title: "Role",
width: 160,
editable: true,
getValue: (row) => row.role,
setValue: (row, nextValue) => ({ ...row, role: nextValue }),
},
{
id: "region",
title: "Region",
width: 160,
editable: true,
getValue: (row) => row.region,
setValue: (row, nextValue) => ({ ...row, region: nextValue }),
},
];
export function EditableGridExample() {
const [rows, setRows] = React.useState<MemberRow[]>([
{ id: 1, name: "Mina", role: "Owner", region: "Seoul" },
{ id: 2, name: "Joon", role: "Editor", region: "Tokyo" },
]);
return (
<DataGrid
rows={rows}
columns={columns}
onRowsChange={(nextRows) => setRows(Array.from(nextRows))}
height={420}
ariaLabel="Editable member grid"
/>
);
}