Skip to main content

Table

TkTable is a component that allows you to display data in a tabular manner. It's generally called a datatable.

import { TkTable } from '@takeoff-ui/react'

Basic

The basic features of the TkTable component are demonstrated.

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
];

return (
<div style={{ padding: "8px" }}>
<TkTable columns={column} data={basicData} />
</div>
);

Striped

To enable striped mode, set the striped prop to true. This feature displays rows with alternating background colors.

View Code
<TkTable columns={column} data={basicData} striped />

Header Type

The headerType prop is used to change the style of the table headers and allows you to customize the headers' style with values such as basic, primary, and dark.

headerType is basic

View Code
<TkTable columns={column} data={headerTypeData} headerType="basic" />

Selection

This feature allows users to select rows in the table. The selected rows are visually highlighted and can be used for various actions.

[{"id":"h456wer53","name":"Bracelet","category":"Clothing","quantity":45}]

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
];
const [selectionList, setSelectionList] = useState();
const [mode, setMode] = useState();
return (
<div style={{ padding: "8px" }}>
<TkTable
columns={column}
data={basicData}
dataKey="id"
selection={selectionList}
selectionMode={mode}
onTkSelectionChange={(e: CustomEvent) =>
setSelectionList(e.detail)
}
/>
<p>{JSON.stringify(selectionList)}</p>
</div>
);

Filter and Sorting

The table supports three types of filtering:

  • Text input filtering: Allows free text search in columns
  • Checkbox filtering: Enables selecting multiple values from predefined options
  • Radio filtering: Allows selecting a single value from predefined options

The filtering type can be configured using the filterType property in column definition. For checkbox and radio filters, you need to provide filterOptions with value-label pairs.



View Code
const column: ITableColumn[] = [
{
field: 'id',
header: 'Id',
},
{
field: 'name',
header: 'Input Filter',
searchable: true,
sortable: true,
sorter: (a: any, b: any) => (a.name > b.name ? 1 : -1),
filter: (value: string, row: any) =>
row.name
.toString()
.toLowerCase()
.indexOf(value.toString().toLowerCase() as string) > -1,
},
{
field: 'status',
header: 'Checkbox Filter',
searchable: true,
filterType: 'checkbox',
filterOptions: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'pending', label: 'Pending' },
],
filterElements: {
icon: 'filter_list',
optionsSearchInput: { show: true, placeholder: 'Filter' },
},
},
{
field: 'group',
header: 'Radio Filter',
searchable: true,
filterType: 'radio',
filterOptions: [
{ value: 'group 1', label: 'Group 1' },
{ value: 'group 2', label: 'Group 2' },
{ value: 'group 3', label: 'Group 3' },
],
filterElements: {
optionsSearchInput: { show: true, placeholder: 'Filter' },
},
},
{
field: 'quantity',
header: 'Quantity',
sortable: true,
sorter: (a: any, b: any) =>
Number(a.quantity) > Number(b.quantity) ? 1 : -1,
},
{
field: 'date',
header: 'Date',
sortable: true,
searchable: true,
sorter: (a: any, b: any) => (a.date > b.date ? 1 : -1),
filterType: 'datepicker',
filterElements: {
optionsSearchDatepicker: {
label: 'Choose a date',
dateFormat: 'yyyy-MM-dd',
size: 'small',
},
},
},
];


const data = [
{
id: 'f230fh0g3',
name: 'Bamboo Watch',
status: 'active',
group: 'group 1',
quantity: 24,
date: '2025-09-07',
},
{
id: 'nvklal433',
name: 'Black Watch',
status: 'inactive',
group: 'group 2',
quantity: 42,
date: '2025-09-08',
},
{
id: 'zz21cz3c1',
name: 'Blue Band',
status: 'active',
group: 'group 3',
quantity: 87,
date: '2025-09-09',
},
{
id: '244wgerg2',
name: 'Blue T-Shirt',
status: 'pending',
group: 'group 1',
quantity: 12,
date: '2025-09-10',
},
{
id: 'h456wer53',
name: 'Bracelet',
status: 'inactive',
group: 'group 2',
quantity: 45,
date: '2025-09-11',
},
];

return <TkTable columns={column} data={data} />;

Multi Sorting

This feature allows users to sort the table by multiple columns simultaneously. Users can click on the column headers to apply sorting, and the order of sorting can be adjusted by clicking on the headers in the desired sequence.


View Code
const column: ITableColumn[] = [
{
field: 'id',
header: 'Id',
},
{
field: 'name',
header: 'Name',
sortable: true,
sorter: (a: any, b: any) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
},
},
{
field: 'status',
header: 'Status',
sortable: true,
sorter: (a: any, b: any) => {
if (a.status < b.status) return -1;
if (a.status > b.status) return 1;
return 0;
},
},
{
field: 'group',
header: 'Group',
sortable: true,
sorter: (a: any, b: any) => {
if (a.group < b.group) return -1;
if (a.group > b.group) return 1;
return 0;
},
},
{
field: 'quantity',
header: 'Quantity',
sortable: true,
sorter: (a: any, b: any) => {
if (a.quantity < b.quantity) return -1;
if (a.quantity > b.quantity) return 1;
return 0;
},
},
];

const data = [
{
id: 'zz21cz3c1',
name: 'Blue Band',
status: 'active',
group: 'group 3',
quantity: 87,
},
{
id: 'h456wer53',
name: 'Bracelet',
status: 'inactive',
group: 'group 2',
quantity: 45,
},
{
id: 'a789def12',
name: 'Blue Band',
status: 'pending',
group: 'group 1',
quantity: 24,
},
{
id: 'b123ghi34',
name: 'Blue Band',
status: 'inactive',
group: 'group 2',
quantity: 12,
},
{
id: 'c456jkl56',
name: 'Smart Watch',
status: 'active',
group: 'group 3',
quantity: 24,
},
{
id: 'e012pqr90',
name: 'Bracelet',
status: 'active',
group: 'group 1',
quantity: 45,
},
];

return <TkTable columns={column} data={data} multiSort={true} />;

Clear Filters and Sorting

This example demonstrates how to clear all filters and sorting settings programmatically using the clearFilters and clearSorting methods.

View Code
const Example = () => {
const tableRef = useRef(null);

const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
searchable: true,
sortable: true,
sorter: (a: any, b: any) => (a.name > b.name ? 1 : -1),
filter: (value: string, row: any) =>
row.name.toString().toLowerCase().indexOf(value.toString().toLowerCase() as string) > -1,
},
{
field: "category",
header: "Category",
searchable: true,
filterType: 'checkbox',
filterOptions: [
{ value: 'Accessories', label: 'Accessories' },
{ value: 'Clothing', label: 'Clothing' },
{ value: 'Fitness', label: 'Fitness' },
],
},
{
field: "quantity",
header: "Quantity",
sortable: true,
sorter: (a: any, b: any) =>
Number(a.quantity) > Number(b.quantity) ? 1 : -1,
},
];

const handleClearFilters = () => {
tableRef.current?.clearFilters();
// For server-side pagination, after clearing filters,
// serverRequest method is called to trigger tkRequest event
// tableRef.current?.serverRequest();
};

const handleClearSorting = () => {
tableRef.current?.clearSorting();
// For server-side pagination, after clearing filters,
// serverRequest method is called to trigger tkRequest event
// tableRef.current?.serverRequest();
};

return (
<div>
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
<TkButton onClick={handleClearFilters} label="Clear Filters"></TkButton>
<TkButton onClick={handleClearSorting} label="Clear Sorting"></TkButton>
</div>
<TkTable ref={tableRef} columns={column} data={basicData}/>
</div>
);
};

Pagination

Client side pagination

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
searchable: true,
sortable: true,
sorter: (a: any, b: any) => (a.name > b.name ? 1 : -1),
filter: (value: string, row: any) =>
row.name
.toString()
.toLowerCase()
.indexOf(value.toString().toLowerCase() as string) > -1,
},
{
field: "category",
header: "Category",
searchable: true,
sortable: true,
sorter: (a: any, b: any) => (a.category > b.category ? 1 : -1),
filter: (value: string, row: any) =>
row.category
.toString()
.toLowerCase()
.indexOf(value.toString().toLowerCase() as string) > -1,
},
{
field: "quantity",
header: "Quantity",
sortable: true,
sorter: (a: any, b: any) =>
Number(a.quantity) > Number(b.quantity) ? 1 : -1,
},
];
return (
<TkTable
columns={column}
data={data}
paginationMethod="client"
rowsPerPage={5}
totalItems={data.length}
/>
);

Server Side

Server side sorting, filter ang pagination example.

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
searchable: true,
sortable: true,
},
{
field: "category",
header: "Category",
searchable: true,
sortable: true,
},
{
field: "quantity",
header: "Quantity",
sortable: true,
},
];

const tableRef = useRef<HTMLTkTableElement>(null);
const [data, setData] = useState();
const [totalItem, setTotalItem] = useState();
const [rowsPerPage, setRowsPerPage] = useState(5);
const [loading, setLoading] = useState(false);

const handleRequest = async (e: TkTableCustomEvent<ITableRequest>) => {
setLoading(true);
const result: any = await fetchFromServer(
e.detail.currentPage,
e.detail.rowsPerPage,
e.detail.filters,
e.detail.sortField,
e.detail.sortOrder
);

setTotalItem(result?.totalItem);
setRowsPerPage(e.detail.rowsPerPage);
setData(result?.data);
setLoading(false);
};

useEffect(() => {
tableRef.current.serverRequest();
}, []);

return (
<>
<TkButton
icon="refresh"
variant="neutral"
type="text"
onTkClick={async () => {
setLoading(true);
const result: any = await fetchFromServer(1, 5, [], null, null);

setTotalItem(result?.totalItem);
setData(result?.data);
setLoading(false);

tableRef.current!.setCurrentPage(1);
}}
/>
<TkTable
ref={tableRef}
columns={column}
data={data}
paginationMethod="server"
rowsPerPage={rowsPerPage}
totalItems={totalItem}
loading={loading}
onTkRequest={handleRequest}
/>
</>
);

Custom Cell

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
searchable: true,
sortable: true,
},
{
field: "category",
header: "Category",
searchable: true,
sortable: true,
html: (row) => {
return `<tk-badge label="${row.category}" ></tk-badge>`;
},
},
{
field: "quantity",
header: "Quantity",
sortable: true,
},
{
field: "-",
header: "Actions",
html: (row) => {
const tkButton: HTMLTkButtonElement =
document.createElement("tk-button");
tkButton.label = "Detail";
tkButton.type = "text";
tkButton.variant = "info";
tkButton.addEventListener("tk-click", () => {
alert("clicked row: " + JSON.stringify(row));
});
return tkButton;
},
},
];
const [data, setData] = useState();
const [totalItem, setTotalItem] = useState();
const [loading, setLoading] = useState(false);

const handleRequest = async (e: TkTableCustomEvent<ITableRequest>) => {
setLoading(true);
const result: any = await fetchFromServer(
e.detail.currentPage,
e.detail.rowsPerPage,
e.detail.filters,
e.detail.sortField,
e.detail.sortOrder
);
setData(result?.data);
setTotalItem(result?.totalItem);
setLoading(false);
};

return (
<TkTable
columns={column}
data={data}
paginationMethod="server"
rowsPerPage={5}
totalItems={totalItem}
loading={loading}
onTkRequest={handleRequest}
/>
);

Custom Header

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
headerHtml: () => {
return '<div style="color: red;">Custom Header</div>';
},
},
{
field: "category",
header: "Category",
headerHtml: () => {
const checkbox = document.createElement('tk-checkbox');
checkbox.label = 'Checkbox';
checkbox.addEventListener('tk-change', (e) => {
console.log('checkbox status', e.detail);
});
return checkbox;
},
},
{
field: "quantity",
header: "Quantity",
},
];

return (
<div style={{ padding: "8px" }}>
<TkTable columns={column} data={basicData} />
</div>
);

Header Style

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
style: {
background: 'var(--primary-base)',
color: 'white',
},
},
{
field: "name",
header: "Name",
style: {
background: 'var(--primary-base)',
color: 'white',
},
},
{
field: "category",
header: "Category",
style: {
background: '#222530',
color: 'white',
},
},
{
field: "quantity",
header: "Quantity",
style: {
background: '#222530',
color: 'white',
},
},
];

return (
<div style={{ padding: "8px" }}>
<TkTable
columns={column}
data={basicData}
/>
</div>
);

Row/Cell Style

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
];

return (
<div style={{ padding: "8px" }}>
<TkTable
columns={column}
data={basicData}
cellStyle={(row, col) => {
if (col.field == "name" && row.name == "Blue Band") {
return { background: "var(--primary-base)", color: "white" };
}
}}
rowStyle={(row, index) => {
if (row.quantity > 50) {
return {
background: "var(--states-success-sub-base)",
color: "darkgreen",
};
}
if (index % 2 === 0) {
return {
background: "var(--states-info-sub-base)",
};
}
}}
/>
</div>
);

Expanded Rows

View Code
const column: ITableColumn[] = [
{
expander: true,
field: "",
header: "",
},
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "quantity",
header: "Quantity",
sortable: true,
sorter: (a: any, b: any) =>
Number(a.quantity) > Number(b.quantity) ? 1 : -1,
},
];
const [expandedRows, setExpandedRows] = useState<any[]>([]);
const handleExpandedRowsChange = (rows: any[]) => {
console.log(rows);
setExpandedRows([...rows]);
};
const renderExpandedRows = () => {
return expandedRows.map((item, index) => {
return (
<div slot={`expand-content-${item.id}`} key={"expanded-row-" + index}>
<div style={{ display: "flex", gap: "8px" }}>content</div>
</div>
);
});
};
return (
<TkTable
columns={column}
data={data}
dataKey="id"
paginationMethod="client"
rowsPerPage={5}
totalItems={data.length}
expandedRows={expandedRows}
onTkExpandedRowsChange={(e) => handleExpandedRowsChange(e.detail)}
>
{renderExpandedRows()}
</TkTable>
);

Export File

The basic features of the TkTable component are demonstrated.

View Code
const tableRef = useRef<HTMLTkTableElement>(null);

const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
];
const exportOptions = [
{
label: "Pdf",
value: "pdf",
},
{
label: "Excel",
value: "excel",
},
{
label: "Csv",
value: "csv",
},
];
const handleItemClick = (e) => {
tableRef.current?.exportFile({
type: e.detail.value,
fileName: "custom_file_name",
} as ITableExportOptions);
};
return (
<div style={{ padding: "8px" }}>
<TkTable ref={tableRef} columns={column} data={basicData}>
<div slot="header-right">
<TkDropdown
options={exportOptions}
position="bottom-end"
onTkItemClick={handleItemClick}
>
<TkButton
slot="trigger"
label="Export"
icon="keyboard_arrow_down"
iconPosition="right"
type="outlined"
/>
</TkDropdown>
</div>
</TkTable>
</div>
);

Sticky Column

This feature allows the selected columns stay in their position in case of having multiple columns that extands the space.

View Code
const tableRef = useRef<HTMLTkTableElement>(null);

const column: ITableColumn[] = [
{
field: "id",
header: "Id",
fixed: "left",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
{
field: "startDate",
header: "Start Date",
},
{
field: "endDate",
header: "End Date",
},
{
field: "duration",
header: "Dration",
},
{
field: "place",
header: "Place",
},
{
field: "status",
header: "Status",
fixed: "right",
},
];
return (
<div style={{ padding: "8px" }}>
<TkTable ref={tableRef} cardTitle="Sticky" columns={column} data={stickyData}>
</TkTable>
</div>
);

This feature allows the table header to remain fixed at the top when scrolling through large datasets. To enable sticky header functionality, you must provide a height value through the containerStyle prop.

View Code
const tableRef = useRef<HTMLTkTableElement>(null);

const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
{
field: "place",
header: "Place",
},
{
field: "status",
header: "Status",
},
];
return (
<TkTable columns={column} data={stickyData} containerStyle={{height: "300px"}}>
</TkTable>
);

Column Arrangement

This feature allows the user to drag and drop the columns to the desired position.

View Code
const [columns, setColumns] = useState<ITableColumn[]>([
{
field: 'id',
header: 'Id',
},
{
field: 'name',
header: 'Name',
},
{
field: 'category',
header: 'Category',
},
{
field: 'quantity',
header: 'Quantity',
},
]);
const [selectedColumns, setSelectedColumns] = useState<ITableColumn[]>(
columns.filter((item) => ['id', 'name'].includes(item.field)),
);

const handleDragStart = (e: React.DragEvent, index: number) => {
const parentElement = e.currentTarget.closest('div');
if (parentElement) {
e.dataTransfer.setData('text/plain', index.toString());
parentElement.classList.add('dragging');
e.dataTransfer.setDragImage(parentElement, 0, 0);
}
};

const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};

const handleDragEnd = (e: React.DragEvent) => {
const parentElement = e.currentTarget.closest('div');
if (parentElement) {
parentElement.classList.remove('dragging');
}
};

const handleDrop = (e: React.DragEvent, targetIndex: number) => {
e.preventDefault();
const sourceIndex = parseInt(e.dataTransfer.getData('text/plain'));
const newColumns = [...columns];
const [movedItem] = newColumns.splice(sourceIndex, 1);
newColumns.splice(targetIndex, 0, movedItem);
setColumns(newColumns);
setSelectedColumns(
newColumns.filter((item) =>
selectedColumns
.map((selectedCol) => selectedCol.field)
.includes(item.field),
),
);
};
return (
<div className="flex flex-col gap-2">
<TkPopover className="flex justify-end" position="bottom" trigger="click">
<TkButton
variant="neutral"
type="outlined"
slot="trigger"
label="Arrange Columns"
/>
<div slot="content">
<div className="flex flex-col gap-2">
{columns.map((col, index) => (
<div
key={col.field}
className="flex justify-between gap-2 py-2 px-3 hover:bg-gray-100 transition-colors"
>
<TkCheckbox
label={col.header}
value={
selectedColumns.findIndex(
(item) => item.field == col.field,
) > -1
}
onTkChange={(e) => {
if (e.detail) {
setSelectedColumns([...selectedColumns, col]);
} else {
setSelectedColumns(
selectedColumns.filter(
(item) => item.field != col.field,
),
);
}
}}
/>
<TkIcon
icon="drag_indicator"
variant="neutral"
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDrop={(e) => handleDrop(e, index)}
style={{ cursor: 'move' }}
/>
</div>
))}
</div>
</div>
</TkPopover>
<TkTable columns={selectedColumns} data={data} />
</div>
);

Size

This feature allows the user to use alternative sizes.

View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
];

return (
<div style={{ padding: "8px" }}>
<TkTable columns={column} data={basicData} size="small"/>
</div>
);

Empty Data Slot

This feature allows you to customize the content displayed when there are no data in the table.

No data found...
View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
];

const [data, setData] = useState([]);
return (
<div className="p-2">
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
<TkButton label="Load Data" onTkClick={() => setData(basicData)} className="mt-2" />
<TkButton label="Show Empty Data Slot" onTkClick={() => setData([])} className="mt-2" />
</div>
<TkTable columns={column} data={data}>
<div slot="empty-data" className="p-4 text-center text-gray-500">
No data found...
</div>
</TkTable>
</div>
);

This feature allows you to add custom header and footer rows within the table body (<tbody>). You can use the slot attribute to specify whether the row is a header or footer. This is useful for adding summary rows, additional information, or custom styling to specific sections of the table body.

Cell Sum DataThis is a custom header row in tbodyCell Sum DataThis is a custom footer row in tbody
View Code
const column: ITableColumn[] = [
{
field: "id",
header: "Id",
},
{
field: "name",
header: "Name",
},
{
field: "category",
header: "Category",
},
{
field: "quantity",
header: "Quantity",
},
];

return (
<div style={{ padding: "8px" }}>
<TkTable columns={column} data={basicData}>
<tr slot="body-header" style={{ background: 'white', fontWeight: 'bold' }}>
<td style={{ padding: '0 8px' }}>Cell Sum Data</td>
<td colSpan={3} style={{ padding: '0 8px' }}>This is a custom header row in tbody</td>
</tr>
<tr slot="body-footer" style={{ background: 'white', fontWeight: 'bold' }}>
<td style={{ padding: '0 8px' }}>Cell Sum Data</td>
<td colSpan={3} style={{ padding: '0 8px' }}>This is a custom footer row in tbody</td>
</tr>
</TkTable>
</div>
);

Grouped Rows

The TkTable component supports row grouping functionality that allows you to organize table data by specific column values. This feature provides visual grouping with headers showing group names and item counts, and works seamlessly with pagination, sorting, and selection.

Key Features

  • Visual Group Headers: Automatically generated headers for each group showing the group value and count
  • Pagination Support: Groups work correctly with both client-side and server-side pagination
  • Selection Persistence: Row selections are maintained across grouping changes
  • Sorting Integration: Grouped data respects existing sort configurations
  • Event Handling: Listen to grouping changes with the onTkGroupByChange event

Two Implementation Approaches

1. Controlled Grouping (State-driven) Use the groupBy prop with component state for external control. This approach gives you full control over the grouping state and allows integration with other application state management.

2. Uncontrolled Grouping (Method-driven)
Use imperative methods like groupByColumn() and clearGrouping() for internal control. This approach is simpler when you don't need to manage grouping state externally.

🎛️ Controlled Grouping (State-driven)

Current groupBy value: status

🔧 Uncontrolled Grouping (Method-driven)

Grouping is controlled internally via imperative methods. Use the buttons below to trigger grouping changes.

💡 Key Features:

  • Controlled Grouping: Use the groupBy prop with React state for external control
  • Uncontrolled Grouping: Use groupByColumn() and clearGrouping() methods
  • Event Handling: Listen to onTkGroupByChange for grouping state changes
  • Visual Grouping: Rows are visually grouped with headers showing group name and count
  • Pagination Support: Grouping works seamlessly with both client and server-side pagination
  • Selection Persistence: Row selection is maintained across grouping changes
View Code
import { ITableColumn } from '@takeoff-ui/core';
import { TkButton, TkTable } from '@takeoff-ui/react';
import { useRef, useState } from 'react';

function GroupedRowsExample() {
const controlledTableRef = useRef<HTMLTkTableElement>(null);
const uncontrolledTableRef = useRef<HTMLTkTableElement>(null);

// Controlled table state
const [controlledGroupBy, setControlledGroupBy] = useState<string>('status');
const [controlledSelectedRows, setControlledSelectedRows] = useState<any[]>([]);
const [uncontrolledSelectedRows, setUncontrolledSelectedRows] = useState<any[]>([]);

// Sample data
const sampleData = [
{ id: 1, name: 'Website Redesign', status: 'In Progress', priority: 'High', department: 'Design' },
{ id: 2, name: 'API Development', status: 'In Progress', priority: 'High', department: 'Engineering' },
{ id: 3, name: 'Database Migration', status: 'Completed', priority: 'Critical', department: 'Engineering' },
{ id: 4, name: 'Marketing Campaign', status: 'Planning', priority: 'Medium', department: 'Marketing' },
// ... more data
];

const columns: ITableColumn[] = [
{ header: 'Project Name', field: 'name', sortable: true, searchable: true },
{ header: 'Status', field: 'status', sortable: true, searchable: true },
{ header: 'Priority', field: 'priority', sortable: true, searchable: true },
{ header: 'Department', field: 'department', sortable: true, searchable: true },
];

// Controlled grouping handlers
const handleControlledGrouping = (field: string) => {
setControlledGroupBy(field);
};

const clearControlledGrouping = () => {
setControlledGroupBy('');
};

// Uncontrolled grouping handlers (using imperative methods)
const handleUncontrolledGrouping = async (field: string) => {
await uncontrolledTableRef.current?.groupByColumn(field);
};

const clearUncontrolledGrouping = async () => {
await uncontrolledTableRef.current?.clearGrouping();
};

// Event handlers
const handleControlledSelectionChange = (e: any) => {
setControlledSelectedRows(e.detail);
};

const handleControlledGroupByChange = (e: any) => {
console.log('GroupBy changed to:', e.detail || 'none');
};

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{/* Controlled Table */}
<div>
<h3>🎛️ Controlled Grouping (State-driven)</h3>
<p>Current groupBy: <strong>{controlledGroupBy || 'none'}</strong></p>

<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<TkButton onTkClick={() => handleControlledGrouping('status')} label="Group by Status" type="outlined" />
<TkButton onTkClick={() => handleControlledGrouping('priority')} label="Group by Priority" type="outlined" />
<TkButton onTkClick={() => handleControlledGrouping('department')} label="Group by Department" type="outlined" />
<TkButton onTkClick={clearControlledGrouping} label="Clear Grouping" type="text" />
</div>

<TkTable
ref={controlledTableRef}
dataKey="id"
cardTitle="Controlled Table"
rowsPerPage={8}
paginationMethod="client"
columns={columns}
data={sampleData}
selectionMode="checkbox"
selection={controlledSelectedRows}
onTkSelectionChange={handleControlledSelectionChange}
onTkGroupByChange={handleControlledGroupByChange}
groupBy={controlledGroupBy}
striped={true}
/>
</div>

{/* Uncontrolled Table */}
<div>
<h3>🔧 Uncontrolled Grouping (Method-driven)</h3>
<p>Grouping controlled internally via imperative methods</p>

<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<TkButton onTkClick={() => handleUncontrolledGrouping('status')} label="Group by Status" type="outlined" />
<TkButton onTkClick={() => handleUncontrolledGrouping('priority')} label="Group by Priority" type="outlined" />
<TkButton onTkClick={() => handleUncontrolledGrouping('department')} label="Group by Department" type="outlined" />
<TkButton onTkClick={clearUncontrolledGrouping} label="Clear Grouping" type="text" />
</div>

<TkTable
ref={uncontrolledTableRef}
dataKey="id"
cardTitle="Uncontrolled Table"
rowsPerPage={8}
paginationMethod="client"
columns={columns}
data={sampleData}
selectionMode="checkbox"
selection={uncontrolledSelectedRows}
onTkSelectionChange={handleUncontrolledSelectionChange}
onTkGroupByChange={handleUncontrolledGroupByChange}
striped={true}
/>
</div>
</div>
);
}

API

Props

NameTypeDefaultDescription
string''
(row: any, column: ITableColumn)=>anynullProvides a function to customize cell styles. This function takes the row and column information and returns the style object for a specific cell.
ITableColumn[][]The column definitions (Array of Objects)
CSSStylePropertiesnullThe style attribute of container element
any[][]Rows of data to display
stringnullProperty of each row that defines the unique key of each row
(row: any)=>anynullProvides a function to customize expanded row styles. This function takes row information and returns the style object for the expanded row content.
any[][]Specifies which rows are expanded to show additional content.
stringnullColumn field name to group the table data by. When specified, the table will automatically group rows by unique values in this column. Set to null or undefined to disable grouping. This makes the component controlled - changes should be handled via tkGroupByChange event.
"basic", "dark", "primary"'basic'Style to apply to header of table
booleannullDisplays a loading indicator while data is being fetched or processed.
booleanfalseEnables multi-column sorting.
stringnullDefines whether pagination is handled on the client or server side.
"grouped", "outlined", "text"'outlined'The type of the pagination
(row: any, index?: number)=>anynullProvides a function to customize row styles. This function takes row information and row index, and returns the style object for a specific row.
number6Number of items per page.
number[]nullNumber of rows per page options
any[]List of the selected
"checkbox", "radio"nullDetermines how rows can be selected, either with radio buttons (single selection) or checkboxes (multiple selection).
FunctionnullA function that returns true if the row should be disabled
"base", "small", "xsmall"'base'Sets size for the component.
booleanfalseEnables or disables alternating row background colors for easier readability.
numbernullNumber of total items.

Events

NameDescription
tk-cell-editEmitted when a cell is edited.
tk-expanded-rows-changeEmitted when the expanded rows change.
tk-group-by-changeEmitted when the groupBy value changes. Always emitted for both controlled and uncontrolled components. For controlled components, handle this event to update the groupBy prop.
tk-requestEmitted when a request needs to be made to the server.
tk-row-clickEmitted when a row is clicked.
tk-selection-change

Methods

NameDescription
clearFiltersClears all filters or specific column filters
clearGroupingClears the current grouping and returns to normal table view Always emits tkGroupByChange event with null value. For uncontrolled components, also clears internal state.
clearSortingClears all sorting for server side pagination
exportFileExports the table data to a file
getFiltersReturns the current filters
getSortingReturns the current sorting settings
groupByColumnGroups table data by the specified column field Creates group header rows that display the unique value and count of items in that group. For example, if you have a 'status' column with values 'Open' and 'Closed', this will create group headers like "Open (5)" and "Closed (3)". Always emits tkGroupByChange event. For uncontrolled components, also updates internal state.
runFiltersApplies the current filters to the data for client side pagination
serverRequestAllows tk-request event to be triggered manually
setCurrentPageSets the current page for pagination
setFiltersSets the current filter settings
setSortingSets the current sorting settings

Slots

NameDescription
body-footerCustom independent rows at the bottom of tbody (e.g., totals, summary, or additional data rows)
body-headerCustom independent rows at the top of tbody (e.g., summary, totals, or custom data rows)
empty-dataSet how the table will appear when there is no data

Interfaces

ITableColumn

Defines the columns for the table

interface ITableColumn {
/** Defines the field for the column */
field: string;
/** Defines heading for the column */
header: string;
/** Defines sub heading for the column */
subHeader?: string;
/** Defines width for the column */
width?: string;
/** Indicates if the column supports sorting */
sortable?: boolean;
/** Custom sort function for the column, mandatory when using client-side sorting. */
sorter?: Function;
/** Custom filter function for the column, mandatory when using client-side filtering. */
filter?: Function;
/** Indicates if the column is searchable */
searchable?: boolean;
/** Indicates if the column is editable */
editable?: boolean;
/** Specifies the input type for editable columns */
inputType?: string;
/** Indicates if the column contains selection checkboxes */
selectColumn?: boolean;
/** Indicates if the column acts as an expander */
expander?: boolean;
/** Custom rendering function for HTML content in the column header */
headerHtml?: Function;
/** Custom rendering function for HTML content in the column cells */
html?: Function;
/** */
fixed?: 'left' | 'right';
/** Allows styling to be applied to the th element of the column */
style?: CSSStyleProperties;
/** When true, search and sort icons will only be displayed when hovering over the th element */
showIconsOnHover?: boolean;
/** Defines the filter type for this column (text, checkbox or radio) */
filterType?: 'text' | 'checkbox' | 'radio' | 'datepicker';
/** Defines options for checkbox or radio filter type */
filterOptions?: IFilterOption[];
/** Defines the label of the buttons */
filterButtons?: {
searchButton?: { label?: string };
cancelButton?: { label?: string };
selectAllCheckbox?: { label?: string };
};
filterElements?: {
icon?: string;
searchInput?: { placeholder?: string };
searchButton?: { label?: string };
cancelButton?: { label?: string };
selectAllCheckbox?: { label?: string };
optionsSearchInput?: { show?: boolean; placeholder?: string };
optionsSearchDatepicker?: {
label?: string;
placeholder?: string;
mode?: 'single' | 'range';
dateFormat?: string;
timeFormat?: '24' | '12';
minDate?: string;
maxDate?: string;
hourStep?: number;
minuteStep?: number;
locale?: string;
showTimePicker?: boolean;
size?: 'small' | 'base' | 'large';
};
};
headerActionsOptions?: {
direction?: 'vertical' | 'horizontal';
};
}

ITableRequest

It is the return type of the tkRequest event.

interface ITableRequest {
/** The current page number */
currentPage: number;
/** The total number of pages */
totalPages: number;
/** The starting index of the items on the current page */
startItem: number;
/** The ending index of the items on the current page */
endItem: number;
/** The number of rows per page */
rowsPerPage: number;
/** The field by which the table is sorted */
sortField?: string;
/** The order of sorting: 'asc' or 'desc' */
sortOrder?: string;
/** Array of sort information for multi-sort functionality. When multiple sorts are applied, they are processed in priority order. */
sorts?: ITableSort[];
/** A list of filters applied to the table */
filters: ITableFilter[];
}

ITableCellEdit

It is the return type of the tkCellEdit event.

interface ITableCellEdit {
/** The unique identifier of the row being edited. Contains the value of the dataKey prop in the edited row */
rowId: string;
/** The index of the row being edited */
rowIndex: number;
/** The field being edited */
field: string;
/** The new value for the field */
value: string;
}

ITableExportOptions

interface ITableExportOptions {
/** only works when type is `pdf`. Default value is `vertical` */
orientation?: 'horizontal' | 'vertical';
/** */
fileName?: string;
/** */
type?: 'csv' | 'pdf' | 'excel';
/** `all` working only client side pagination. Default value is `current-page` */
scope?: 'current-page' | 'selected' | 'all';
/** Ignore Columns Fields array for only excel export */
ignoreColumnsFields?: string[];
/** Columns for only excel export */
columns?: ITableExportExcelColumn[];
/** */
externalData?: any[];
}

ITableFilter

Represents a filter applied to a table

interface ITableFilter {
/** The value of the filter - string for text/radio/datepciker filter, string array for checkbox filter, IDateSelection for datepicker filter */
value?: string | string[] | IDateSelection;
/** The field to which the filter is applied */
field: string;
/** The type of the filter (text, checkbox, radio, or datepicker) */
type?: 'text' | 'checkbox' | 'radio' | 'datepicker';
}

ITableSort

interface ITableSort {
/** The field name being sorted */
field: string;
/** The sort direction */
order: 'asc' | 'desc';
}

IFilterOption

Defines options for checkbox filter

interface IFilterOption {
/** The value of the option */
value: string;
/** The display label of the option */
label?: string;
}

ICustomElement

interface ICustomElement {
ref: HTMLElement;
element: HTMLElement;
}

ITableExportExcelColumn

interface ITableExportExcelColumn {
header: string;
field: string;
width: number;
}

ITableGroup

Represents a group of table rows with associated metadata

interface ITableGroup {
/** The value that this group represents (e.g., "Active", "Completed") */
groupValue: any;
/** The number of rows in this group */
groupCount: number;
/** The array of row data objects belonging to this group */
rows: any[];
}