Overview
The Akitable component, is designed to manage and display complex datasets in
React applications efficiently. It integrates with Ant Design for robust UI
elements and extends functionality with custom theming, pagination, and row
selection capabilities.
Key Features
- Dynamic Data Rendering: Easily render data arrays with customizable columns.
- Integrated Pagination: Managed through the
paginationprop for smooth navigation. - Flexible Row Selection: Pass
rowSelectiondirectly to enable checkbox/radio selection independently ofactions. - Customizable Styling: Adapt the table to different UI themes, with options for background removal and scroll behavior.
- Extensible Design: Works seamlessly with
header,footer, andactionsprops.
Example Usage
import type {
AkitableAction,
AkitableColumn,
AkitablePaginatedData,
AkitablePaginationProps,
SorterChangeEvent
} from '@akinon/akitable';
import { Akitable } from '@akinon/akitable';
const results = [
{
key: '1',
name: 'Mike',
age: 32
},
{
key: '2',
name: 'John',
age: 42
}
];
const MyComponent = () => {
const data: AkitablePaginatedData = {
count: results.length,
results
};
const columns: AkitableColumn[] = [
{
title: 'Name',
dataIndex: 'name',
key: 'name'
},
{
title: 'Age',
dataIndex: 'age',
key: 'age'
}
];
const actions: AkitableAction[] = [
{
label: 'Delete',
onSelect: (selectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys: ', selectedRowKeys);
}
}
// Additional actions...
];
const pagination: AkitablePaginationProps = {
page: 1,
size: 20
};
const handleSorterChange: SorterChangeEvent = (sorter) => {
console.log('Sorter changed: ', sorter);
};
return (
<Akitable
isLoading={false}
data={data}
columns={columns}
actions={actions}
pagination={pagination}
rowKey="key"
onChangeSorter={handleSorterChange}
// Additional Akitable props...
/>
);
};
export default MyComponent;Akitable Props
Akitable props provide various options for configuring the table component:
| Property | Description | Type |
|---|---|---|
isLoading | Indicates if the data is currently loading. | boolean |
data | Array of data objects to display. | AkitableData[] | AkitablePaginatedData |
columns | Configuration for the table columns. | AkitableColumn[] |
actions | Array of actions that can be performed on selected rows. | AkitableAction[] |
header | Manages the display of the table’s header. | AkitableHeaderProps | undefined |
footer | Manages the display of the table’s footer. | AkitableFooterProps | undefined |
emptyState | Custom empty state configuration for the table. | AkitableEmptyStateProps | undefined |
rowKey | Row’s unique key. | string |
pagination | Pagination settings to control the page navigation. Optionally takes pageSizeOptions to replace the “items per page” list. | AkitablePaginationProps | undefined |
onRowClick | Configuration for row click features. | RowClickEvent | undefined |
onRowEdit | Configuration for row edit features. | RowEditCallback | undefined |
actionColumn | Action column appended as the last column, replacing the default edit column. | AkitableActionColumn | undefined |
onChangeSorter | Called when table sorting is changed. | SorterChangeEvent | undefined |
rowSelection | Row selection configuration. Enables checkbox/radio selection regardless of whether actions are defined. When actions are also present, selectedRowKeys and onChange are managed internally; all other rowSelection fields are merged. | AkitableRowSelection | undefined |
onPaginationChanged | Called when the page number or page size is changed. | PaginationChangeEvent | undefined |
paginationShowTotal | Custom render function for the pagination total label (e.g. “1-20 of 100 items”). | (total: number, range: [number, number]) => ReactNode | undefined |
Types
interface AkitableData {
[key: string]: any;
}
interface AkitablePaginatedData {
count: number;
next?: string | null;
previous?: string | null;
results: AkitableData[];
}import type { TableColumnType } from 'antd';
interface AkitableColumn extends TableColumnType<AkitableData> {
copyable?: boolean;
editable?: boolean;
}You can learn about TableColumnType type
here .
interface AkitableAction {
label: string;
onSelect: (selectedRowKeys: React.Key[]) => void;
}interface AkitableActionColumnRenderParams {
record: AkitableData;
isEditing: boolean;
isEditLoading: boolean;
hasEditFormError: boolean;
handleStartEdit: (e?: React.MouseEvent) => void;
handleSaveEditingItem: () => Promise<void>;
handleCancelEditingItem: () => void;
}
interface AkitableActionColumn {
title?: ReactNode;
width?: number | string;
fixed?: 'left' | 'right' | boolean;
className?: string;
align?: 'left' | 'right' | 'center';
render?: (params: AkitableActionColumnRenderParams) => ReactNode;
items?: AkitableActionItem[];
onNavigate?: (
href: string,
record: AkitableData,
event: React.MouseEvent
) => void;
}type AkitableActionItemValue<T> = T | ((record: AkitableData) => T);
// Every other `@akinon/ui-button` prop (`icon`, `iconSize`, `type`, `danger`,
// `tooltip`, `loading`, `ghost`, `block`, `className`, `data-*`, …) is accepted
// and forwarded to the rendered button. `size` is fixed to `small` by the
// action column, so it is not accepted here.
interface AkitableActionItem
extends Omit<
IButtonProps,
'children' | 'disabled' | 'hidden' | 'href' | 'onClick' | 'size'
> {
key?: string;
label?: ReactNode;
href?: AkitableActionItemValue<string>;
onClick?: (record: AkitableData, event: React.MouseEvent) => void;
startEdit?: boolean;
disabled?: AkitableActionItemValue<boolean>;
hidden?: AkitableActionItemValue<boolean>;
}interface AkitableHeaderProps {
title?: string;
extra?: ReactNode;
}interface AkitableFooterProps {
extra?: ReactNode;
}interface AkitableEmptyStateProps {
icon?: ReactNode;
description?: ReactNode;
}// The default sizes stay as literals for editor suggestions, but any number is
// accepted so a custom `pageSizeOptions` list is not limited to them.
type AkitablePageSizes = 20 | 50 | 100 | 250 | (number & NonNullable<unknown>);
interface AkitablePaginationProps {
page: number;
size: AkitablePageSizes;
pageSizeOptions?: readonly AkitablePageSizes[];
}Customizing the page size options
The “items per page” dropdown offers [20, 50, 100, 250] by default. Pass
pageSizeOptions to replace that list — useful when the backend does not
support every size. Omitting it (or passing an empty array) keeps the defaults.
Repeated sizes are dropped from the list, but the given order is preserved — the dropdown presents the sizes exactly as they are passed.
size accepts any number, but it must be a positive integer: a 0, negative or
fractional size would leave the pager with a nonsensical page count, so the
pagination renders the first offered option instead — and reports that size back
through onPaginationChanged on the next interaction.
import {
AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS,
type AkitablePaginationProps
} from '@akinon/akitable';
// Only the sizes the backend supports.
const pagination: AkitablePaginationProps = {
page: 1,
size: 20,
pageSizeOptions: [20, 50, 100]
};
// Or derive the list from the defaults.
const derived: AkitablePaginationProps = {
page: 1,
size: 20,
pageSizeOptions: AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS.filter(
size => size !== 250
)
};type RowClickEvent = (
record: AkitableData,
event?: React.MouseEvent<HTMLElement>,
rowIndex?: number
) => void;type RowEditCallback = (
modifiedRecord: AkitableData,
payload: AkitableData
) => void | Promise<void>;type PaginationChangeEvent = (page: number, size: number) => void;type SortOrder = 'descend' | 'ascend' | null;
interface SorterResult {
column?: AkitableColumn;
order?: SortOrder;
field?: string | readonly string[];
columnKey?: string;
}
type TableOnChangeSorter = SorterResult | SorterResult[];
type SorterChangeEvent = (sorter: TableOnChangeSorter) => void;enum AkitableRowStatus {
PENDING = 'pending',
ERROR = 'error'
}type AkitableRowSelectionType = 'checkbox' | 'radio';
type AkitableRowSelectMethod = 'all' | 'none' | 'invert' | 'single' | 'multiple';
interface AkitableRowSelection {
align?: 'left' | 'center' | 'right';
checkStrictly?: boolean;
columnTitle?: ReactNode | ((originalNode: ReactNode) => ReactNode);
columnWidth?: string | number;
fixed?: boolean | 'left' | 'right';
getCheckboxProps?: (record: AkitableData) => Record<string, unknown>;
getTitleCheckboxProps?: () => Record<string, unknown> & React.AriaAttributes;
hideSelectAll?: boolean;
preserveSelectedRowKeys?: boolean;
renderCell?: (checked: boolean, record: AkitableData, index: number, originNode: ReactNode) => ReactNode;
selectedRowKeys?: React.Key[];
selections?: object[] | boolean;
type?: AkitableRowSelectionType;
onChange?: (selectedRowKeys: React.Key[], selectedRows: AkitableData[], info: { type: AkitableRowSelectMethod }) => void;
onSelect?: (record: AkitableData, selected: boolean, selectedRows: AkitableData[], nativeEvent: Event) => void;
}Navigating Actions (Open in a New Tab)
Actions that navigate somewhere should be declared through actionColumn.items rather than a
custom render. Akitable renders every item that has an href as a real <a href> element,
so the browser’s own gestures work on it:
| Gesture | Handled by | Result |
|---|---|---|
| Left click | Akitable → onNavigate | Client-side navigation, no page reload |
| Middle click | The browser | Opens in a new tab |
Cmd/Ctrl/Shift + click | The browser | Opens in a new tab or window |
| Right click → open in a new tab | The browser | Opens in a new tab |
A plain left click is intercepted (preventDefault) and delegated to the item’s own onClick
if it has one, otherwise to the column’s onNavigate — which keeps SPA routing intact. When
neither is defined the click is left alone and the browser follows the href as a regular
link. No target is set on the anchor, because that would make a plain left click open a new
tab as well.
Non-navigating actions (delete, toggles, …) use onClick instead of href and render as
plain buttons, so they are unaffected.
An item that is disabled or loading renders as a plain button as well, even when it has an
href: antd ignores a Button’s click handler in both states, so an anchor left in place would
be followed on a middle click while a plain left click did nothing. Clicking an action never
triggers the table’s onRowClick, in any of these states.
<Akitable
data={data}
columns={columns}
rowKey="id"
actionColumn={{
title: 'Actions',
fixed: 'right',
onNavigate: (href) => navigate(href),
items: [
{
key: 'edit',
icon: 'edit',
tooltip: 'Edit',
type: 'link',
href: (record) => `/products/${record.id}/edit`
},
{
key: 'delete',
icon: 'bin',
tooltip: 'Delete',
type: 'text',
danger: true,
onClick: (record) => remove(record.id)
}
]
}}
/>href, disabled and hidden accept either a static value or a function of the row, so
each row can resolve its own target and state.
Since the href becomes a real anchor attribute, it must be a URL the browser can resolve on
its own — for example /products/1, or #/products/1 when a hash router is used.
Because an href is typically derived from record data, it is validated rather than trusted: a
real anchor is followed natively on middle click and Cmd/Ctrl + click, where no click
handler runs to intercept it. Only relative URLs (/products/1, products/1, #/products/1,
?page=2) and the http, https, mailto and tel schemes are accepted. Everything else is
rejected — javascript: above all, but also a protocol-relative URL (//evil.com, plus the
\\evil.com / /\evil.com shapes browsers parse the same way), an empty or whitespace-only
value, and an internal routing key shaped like catalog:detail, which parses as the scheme
catalog:.
A rejected value is dropped silently: nothing is logged and the item renders as a plain
button. Check the values your href builder can produce rather than waiting for a console
message — record => record.detailUrl ?? '' is an easy slip that turns a link into a button.
Precedence and Backwards Compatibility
render takes precedence over items when both are provided, and a warning is logged. This
keeps every existing actionColumn.render usage working unchanged.
Note that anything returned from render is opaque to Akitable, so a <button onClick>
rendered there will not gain new-tab support. Either migrate the navigating actions to
items, or render your own real <a href> elements inside render.
To put the row into edit mode from an item, set startEdit: true instead of an onClick.
This requires the table’s onRowEdit to be defined, since that is what saves the row. While a
row is being edited, Akitable renders its built-in save and cancel actions in place of the
items.