@axboot/datagrid
v1.10.6
Published
DataGrid, DataSheet for React
Downloads
1,904
Readme
@axboot/datagrid
DataGrid, DataSheet for React
License
This package is distributed under a commercial license. Use of the compiled package requires a valid commercial license agreement. Source code access does not grant permission to modify, redistribute, or create derivative works. See LICENSE for details.
Install
npm i @axboot/datagridDevelopment
npm i
npm run devOpen http://localhost:5173 with your browser to see the demo app.
Internal notes:
Testing
# Unit tests (Vitest)
npm test
# Unit tests in watch mode
npm run test:watch
# Consumer compatibility test (install + cjs/esm/types check)
npm run test:library:consumers
# E2E tests (Playwright)
npm run test:e2e
# E2E tests with UI runner
npm run test:e2e:uiBuild
# Demo app bundle (Vite)
npm run build
# Publishable library bundle (CJS + ESM + types + style)
npm run build:libraryPublish
# Build dist/cjs, dist/esm, dist/types and dist/package.json
npm run build:library
# Dry-run package file list from dist
npm run pack:library
# Publish dist package manually
npm run publish:libraryGitHub Actions Auto Publish
- Workflow file:
.github/workflows/publish-npm.yml - Trigger: push tag
v*(example:v1.6.4) or manualworkflow_dispatch - Required secret:
NPM_TOKEN - Safety check: tag version must match
package.jsonversion
Styling
Import the library stylesheet once in your app:
import '@axboot/datagrid/style.css';The grid uses CSS custom properties scoped to [role='ax-datagrid'], so you can override the theme globally or for a specific wrapper.
Default variables:
[role='ax-datagrid'] {
--axdg-primary-color: #3b82f6;
--axdg-header-bg: #f3f4f5;
--axdg-header-color: #222;
--axdg-header-font-weight: 500;
--axdg-header-hover-bg: #e2e5e5;
--axdg-header-group-bg: #e9e9e9;
--axdg-footer-bg: #f3f4f5;
--axdg-summary-bg: #eaeef6;
--axdg-border-color-base: #d2d5d9;
--axdg-border-color-light: #d2d5d9;
--axdg-border-color-subtle: #e8ebef;
--axdg-header-separator-color: #dde2e8;
--axdg-border-radius: 4px;
--axdg-row-selector-color: #ffffff;
--axdg-body-bg: #ffffff;
--axdg-body-odd-bg: #f8f8f8;
--axdg-body-hover-bg: #f3f4f5;
--axdg-body-hover-odd-bg: #eeeeee;
--axdg-body-active-bg: #e6f6ff;
--axdg-cell-selected-bg: var(--axdg-body-active-bg);
--axdg-cell-selected-border-color: rgba(59, 130, 246, 0.78);
--axdg-body-color: #444;
--axdg-scroll-size: 11px;
--axdg-scroll-bg: #ffffff;
--axdg-scroll-track-bg: #f6f6f6;
--axdg-scroll-thumb-radius: 100px;
--axdg-scroll-thumb-bg: #c7ccda;
--axdg-scroll-thumb-hover-bg: #a1a3a6;
--axdg-scroll-corner-bg: #c7ccda;
--axdg-scroll-corner-radius: 5px;
--axdg-loading-bg: rgba(163, 163, 163, 0.1);
--axdg-loading-color: rgba(0, 0, 0, 0.1);
--axdg-loading-second-color: #767676;
--axdg-page-number-active-border-radius: 4px;
}Common customization targets:
| Area | Variables |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Primary accents | --axdg-primary-color |
| Header | --axdg-header-bg, --axdg-header-color, --axdg-header-font-weight, --axdg-header-hover-bg, --axdg-header-group-bg, --axdg-header-separator-color |
| Borders | --axdg-border-color-base, --axdg-border-color-light, --axdg-border-color-subtle, --axdg-border-radius |
| Body rows | --axdg-body-bg, --axdg-body-odd-bg, --axdg-body-hover-bg, --axdg-body-hover-odd-bg, --axdg-body-active-bg, --axdg-body-color |
| Cell selection | --axdg-cell-selected-bg, --axdg-cell-selected-border-color |
| Summary/Footer | --axdg-summary-bg, --axdg-footer-bg |
| Scrollbars | --axdg-scroll-size, --axdg-scroll-bg, --axdg-scroll-track-bg, --axdg-scroll-thumb-radius, --axdg-scroll-thumb-bg, --axdg-scroll-thumb-hover-bg, --axdg-scroll-corner-bg, --axdg-scroll-corner-radius |
| Loading overlay | --axdg-loading-bg, --axdg-loading-color, --axdg-loading-second-color |
| Pagination | --axdg-page-number-active-border-radius |
selectedRowKey row highlighting uses --axdg-body-active-bg. Cell selection uses --axdg-cell-selected-bg for the fill and --axdg-cell-selected-border-color for the selection edge. By default, --axdg-cell-selected-bg references --axdg-body-active-bg so row selection and cell selection stay in the same color tone.
Example: scope a custom blue selection theme to one grid wrapper.
.my-grid [role='ax-datagrid'] {
--axdg-primary-color: #2563eb;
--axdg-body-active-bg: #e0f2fe;
--axdg-cell-selected-bg: var(--axdg-body-active-bg);
--axdg-cell-selected-border-color: rgba(37, 99, 235, 0.72);
}Usage
- codesandbox DEMO : https://codesandbox.io/p/devbox/basic-example-5ch6kt?embed=1&file=%2Fsrc%2FApp.tsx
Basic Example
import * as React from 'react';
import { AXDataGrid, AXDGColumn } from '@axboot/datagrid';
interface IListItem {
id: string;
title: string;
writer: string;
createAt: string;
}
const list = Array.from({ length: 1000 }).map((_, i) => ({
values: {
id: `ID_${i}`,
title: `title_${i}`,
writer: `writer_${i}`,
createAt: `2022-09-08`,
},
}));
function BasicExample() {
const [columns, setColumns] = React.useState<AXDGColumn<IListItem>[]>([
{ key: 'id', label: 'ID', width: 120 },
{
key: 'title',
label: '제목',
width: 260,
itemRender: ({ values }) => (
<>
{values.writer} / {values.title}
</>
),
},
{ key: 'writer', label: '작성자', width: 120 },
{ key: 'createAt', label: '작성일', width: 140 },
]);
const [checkedRowKeys, setCheckedRowKeys] = React.useState<React.Key[]>([]);
return (
<div style={{ fontSize: 13 }}>
<AXDataGrid<IListItem>
width={720}
height={420}
data={list}
columns={columns}
rowKey={'id'}
onChangeColumns={(columnIndex, { width, columns }) => {
console.log('onChangeColumns', columnIndex, width, columns);
setColumns(columns);
}}
rowChecked={{
checkedRowKeys,
onChange: (checkedIndexes, checkedRowKeys, checkedAll) => {
console.log('rowChecked changed', checkedIndexes, checkedRowKeys, checkedAll);
setCheckedRowKeys(checkedRowKeys);
},
}}
/>
</div>
);
}
export default BasicExample;Important Data/Column Rules
- Row data type is
AXDGDataItem<T>and the real domain model is always insideitem.values. AXDGColumn.keysupports bothstringandstring[].key: 'writer'key: ['user', 'profile', 'name'](nested value path)
itemRenderreceives both:value: current cell value bycolumn.keyvalues: full row model (T)
columns[i].leftis internal layout value computed byAXDataGrid; do not manage it manually.
Cell Selection and Clipboard
Cells can be selected by mouse drag. Ctrl+C or Cmd+C copies selected cell text using tab (\t) between columns and carriage return (\r) between rows.
Cell selection is kept when the user clicks inside the grid, including empty body space or the grid scrollbar. By default, selection is cleared when Escape is pressed or when the user clicks outside the grid. Use cellSelectionOptions to change those clear rules:
<AXDataGrid
width={700}
height={400}
columns={columns}
data={data}
cellSelectionOptions={{
clearOnEscape: false,
clearOnOutsideClick: false,
}}
/>By default, copied text is read from column.key:
{ key: 'title', label: 'Title', width: 240 }If a column renders a custom component or needs a different clipboard value, define getClipboardText on the column. getClipboardText is used before falling back to the column.key value.
const columns: AXDGColumn<IListItem>[] = [
{
key: 'title',
label: 'Title',
width: 260,
itemRender: ({ values }) => (
<>
{values.writer} / {values.title}
</>
),
getClipboardText: ({ values }) => `${values.writer}\t${values.title}`,
},
{
key: ['user', 'profile', 'name'],
label: 'User',
width: 160,
getClipboardText: ({ value, values }) => value ?? values.writer,
},
];Pivot
Pass pivot to render the grid as a derived pivot table. The API follows the Excel-style field areas:
rows: row fieldscolumns: column fieldsvalues: value fields and aggregate rules
When pivot is active, row-based grid UI is disabled internally because the rendered rows are derived summary rows. This includes frozen columns, line numbers, row check controls, sorting, column sorting, editing, pagination, summary, cell merge, reorder, and row/cell change callbacks.
import * as React from 'react';
import { AXDataGrid, AXDGColumn, AXDGProps } from '@axboot/datagrid';
interface SalesItem {
region: string;
product: string;
quarter: string;
sales: number;
quantity: number;
}
const data = [
{ values: { region: 'North', product: 'Desk', quarter: 'Q1', sales: 1200, quantity: 12 } },
{ values: { region: 'North', product: 'Desk', quarter: 'Q2', sales: 1400, quantity: 10 } },
{ values: { region: 'South', product: 'Chair', quarter: 'Q1', sales: 900, quantity: 8 } },
];
const columns: AXDGColumn<SalesItem>[] = [
{ key: 'region', label: 'Region', width: 120 },
{ key: 'product', label: 'Product', width: 140 },
{ key: 'quarter', label: 'Quarter', width: 100 },
{ key: 'sales', label: 'Sales', width: 120, align: 'right' },
{ key: 'quantity', label: 'Quantity', width: 120, align: 'right' },
];
const pivot: AXDGProps<SalesItem>['pivot'] = {
rows: [{ key: 'product', label: 'Product', width: 140 }],
columns: [{ key: 'region', label: 'Region' }],
values: [
{
key: 'sales',
label: 'Sales',
aggregate: 'sum',
itemRender: ({ value, columnValues, sourceItems }) => {
const text = `$${Number(value).toLocaleString()}`;
if (columnValues[0] === 'South') {
return <strong title={`${sourceItems.length} source rows`}>{text}</strong>;
}
return <span title={`${sourceItems.length} source rows`}>{text}</span>;
},
getClipboardText: ({ value }) => Number(value).toLocaleString(),
},
{
key: 'quantity',
label: 'Quantity',
aggregate: 'sum',
itemRender: ({ value }) => <>{Number(value).toLocaleString()} ea</>,
},
],
emptyValue: 0,
};
function PivotExample() {
return (
<AXDataGrid<SalesItem>
width={900}
height={420}
columns={columns}
data={data}
pivot={pivot}
variant={'vertical-bordered'}
/>
);
}Built-in aggregate values:
| Aggregate | Description |
| --------- | --------------------------------- |
| sum | Numeric sum. This is the default. |
| count | Number of source values. |
| avg | Numeric average. |
| min | Numeric minimum. |
| max | Numeric maximum. |
| first | First source value. |
You can also pass a custom aggregate function:
const pivot: AXDGProps<SalesItem>['pivot'] = {
rows: [{ key: 'product', label: 'Product' }],
columns: [{ key: 'quarter', label: 'Quarter' }],
values: [
{
key: 'sales',
label: 'Max online sales',
aggregate: ({ items }) => {
return Math.max(...items.filter(item => item.values.region === 'North').map(item => item.values.sales));
},
},
],
};pivot.values[].itemRender receives the normal cell render props plus pivot context:
| Prop | Description |
| -------------- | ----------------------------------------------- |
| value | Aggregated cell value |
| values | Pivot result row values |
| rowValues | Values for the current pivot row fields |
| columnValues | Values for the current pivot column fields |
| sourceItems | Original AXDGDataItem<T>[] used for this cell |
| pivotValue | The current value-field definition |
| aggregate | The current aggregate setting |
Common Scenarios
1) Frozen Columns
<AXDataGrid width={900} height={500} frozenColumnIndex={2} columns={columns} data={data} />frozenColumnIndex 이전 컬럼(0, 1)은 고정 영역으로 렌더링됩니다.
2) Editable Cell
const columns: AXDGColumn<IListItem>[] = [
{
key: 'title',
label: '제목',
width: 240,
editable: true,
itemRender: ({ editable, value, handleSave, handleCancel }) => {
if (!editable) return <>{value}</>;
return (
<input
autoFocus
defaultValue={value}
onBlur={e => handleSave?.(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') handleSave?.((e.target as HTMLInputElement).value);
if (e.key === 'Escape') handleCancel?.();
}}
/>
);
},
},
];
<AXDataGrid editable editTrigger={'dblclick'} columns={columns} data={data} width={700} height={400} />;3) Sort + Page
<AXDataGrid
width={900}
height={560}
columns={columns}
data={rows}
sort={{
sortParams,
onChange: next => setSortParams(next),
}}
page={{
currentPage,
pageSize,
totalPages,
totalElements,
onChange: (nextPage, nextSize) => {
setCurrentPage(nextPage);
if (nextSize) setPageSize(nextSize);
},
}}
/>Props Reference (AXDataGrid)
아래는 자주 사용하는 Props 중심 정리입니다. 타입의 최종 기준은 @axboot-datagrid/types.ts의 AXDGProps<T>입니다.
Required
| Prop | Type | Description |
| --------- | ------------------------------------ | ---------------- |
| width | number | 그리드 전체 너비 |
| height | number | 그리드 전체 높이 |
| columns | AXDGColumn<T>[] (width optional) | 컬럼 정의 |
Data / Selection / Row Focus
| Prop | Type | Description |
| ----------------- | ----------------------------------- | ------------------------------------------- |
| data | AXDGDataItem<T>[] | 행 데이터 (values 래핑 필수) |
| rowKey | React.Key \| React.Key[] | 행 고유 키 필드 |
| selectedRowKey | React.Key \| React.Key[] | 포커스된 행 키 |
| rowChecked | AXDGRowChecked<T> | 체크박스/라디오 선택 제어 (onChange 필수) |
| getRowClassName | (ri, item) => string \| undefined | 행 단위 className 지정 |
rowSelection/selectedIds는 현재 API가 아닙니다.rowChecked를 사용해야 합니다.
Layout / Rendering
| Prop | Type | Description |
| ------------------- | ---------------------------------- | -------------------------------- |
| headerHeight | number | 헤더 높이 (기본 30) |
| footerHeight | number | 페이지네이션 푸터 높이 (기본 30) |
| summaryHeight | number | summary 높이 (기본 30) |
| itemHeight | number | 본문 행 컨텐츠 높이 (기본 15) |
| itemPadding | number | 행 내부 패딩 (기본 7) |
| frozenColumnIndex | number | 고정 컬럼 경계 인덱스 |
| showLineNumber | boolean | 좌측 라인 번호 표시 |
| variant | 'default' \| 'vertical-bordered' | 스킨 변형 |
| loading | boolean | 전체 오버레이 로딩 |
| spinning | boolean | 바디 스피너 로딩 |
Column / Editing / Events
| Prop | Type | Description |
| ----------------- | -------------------------------------------- | ------------------------------- |
| columnsGroup | AXDGColumnGroup[] | 헤더 그룹 |
| onChangeColumns | (columnIndex, info) => void | 컬럼 폭/순서/그룹 변경 콜백 |
| onChangeData | (index, columnIndex, item, column) => void | 셀 편집 데이터 변경 콜백 |
| editable | boolean | 편집 모드 활성화 |
| editTrigger | 'click' \| 'dblclick' | 편집 진입 트리거 (기본 click) |
| onClick | (params) => void | 셀 클릭 이벤트 |
Extra Features
| Prop | Type | Description |
| ---------------------- | ------------------------------------------------------------ | ----------------------- |
| sort | AXDGSortInfo | 정렬 상태/변경 콜백 |
| columnSortable | boolean | 컬럼 drag sort 제어 |
| page | AXDGPage | 페이지네이션 상태/콜백 |
| summary | { columns, position } | 상/하단 summary 행 |
| cellMergeOptions | { columnsMap } | 셀 병합 옵션 |
| cellSelectionOptions | { clearOnEscape?: boolean; clearOnOutsideClick?: boolean } | 셀 선택 초기화 룰 |
| reorder | AXDGReorderInfo<T> | 행 reorder 설정 |
| pivot | AXDGPivotOptions<T> | 피벗 테이블 렌더링 설정 |
| msg | { emptyList?: string } | 커스텀 메시지 |
Update Note
v1.5
- rowChecked 속성 추가 (rowChecked > isRadio, rowChecked > disabled)
<AXDataGrid<IListItem>
width={containerWidth}
height={containerHeight}
headerHeight={35}
data={sortedList}
columns={columns}
onChangeColumns={(columnIndex, { width, columns }) => {
console.log('onChangeColumnWidths', columnIndex, width, columns);
setColumns(columns);
}}
rowChecked={{
disabled: (ri, item) => ri === 0,
isRadio: true,
checkedRowKeys: checkedKeys,
onChange: (ids, keys, selectedAll) => {
console.log('onChange rowSelection', ids, keys, selectedAll);
setCheckedKeys(keys);
},
}}
sort={{
sortParams,
onChange: sortParams => {
console.log('onChange: sortParams', sortParams);
setSortParams(sortParams);
},
}}
showLineNumber
rowKey={'nation'}
/>V1.4
- columnsGroup 타입변경 기존 columnsIndex: []에서 start, end 지정 형태로 변경되었습니다.
[{ label: '묶음', groupStartIndex: 2, groupEndIndex: 4, align: 'center' }];- onChangeColumns 속성 변경
// onChangeColumns Type
onChangeColumns?: (
columnIndex: number | null,
info: {
width?: number;
columns: AXDGColumn<T>[];
columnsGroup?: AXDGColumnGroup[];
},
) => void;
// onChangeColumns에서 변경된 컬럼과 컬럼 그룹을 받을 수 있습니다
<AXDataGrid
/*...*/
onChangeColumns={(columnIndex, { columns, columnsGroup }) => {
console.log('onChangeColumnWidths', columnIndex, columns, columnsGroup);
setColumns(columns);
setColumnsGroup(columnsGroup);
}}
/>