---
name: ktui-datatable
description: >
KtUI DataTable (KTDataTable) — local/remote data, sorting, filtering, pagination,
checkbox selection, state persistence, fixed layouts, event system, and architecture.
Use this skill when building, debugging, or customizing DataTable components.
---
# KTDataTable — AI Agent Reference
Full reference for the DataTable component in [KtUI](https://ktui.io).
Package: `@keenthemes/ktui`. Class: `KTDataTable`. Root attribute: `data-kt-datatable`.
> **Always prefer [ktui.io/docs/datatable](https://ktui.io/docs/datatable) docs and examples over guessing markup or options.**
---
## 1. Basic Usage
```html
```
```ts
import { KTDataTable } from '@keenthemes/ktui';
const dt = KTDataTable.getInstance(tableEl);
```
---
## 2. Config Options
| Option | Type | Description |
|--------|------|-------------|
| `apiEndpoint` | string | Remote data URL |
| `requestMethod` | string | HTTP method (default `'POST'`) |
| `requestHeaders` | object | Custom headers |
| `mapResponse` | function | Transform API response |
| `mapRequest` | function | Transform request params |
| `pageSize` | number | Rows per page |
| `pageSizes` | number[] | Page size options |
| `stateSave` | boolean | Persist state in localStorage |
| `columns` | object | Column config (render, checkbox, sortType, sortValue, createdCell) |
| `sort` | object | Sort config with classes and callback |
| `search` | object | Search config with delay and callback |
| `pagination` | object | Pagination markup config |
| `loading` | object | Spinner template |
| `checkbox` | object | Row checkbox config (checkedClass, preserveSelection) |
| `lockedLayout` | object | Sticky headers/columns |
| `tableLayout` | string | `'fixed'` for fixed column widths (use with ``) |
| `filter` | object | Column filter config (type, value) |
| `infoEmpty` | string | Empty state HTML (supports innerHTML) |
---
## 3. Programmatic API
```ts
const dt = KTDataTable.getInstance(tableEl);
dt.sort('name');
dt.sort('name', 'desc'); // explicit sort order
dt.goPage(2);
dt.setPageSize(25);
dt.search('query');
dt.setFilter({ column: 'status', type: 'text', value: 'active' });
dt.reload(); // re-fetch from API
dt.redraw(); // re-render current data
dt.getState(); // { page, sortField, sortOrder, pageSize, ... }
dt.check(value); // check a row by value
dt.uncheck(value); // uncheck a row by value
dt.getChecked(); // get array of checked row values
dt.dispose();
```
### Instance management
| Static method | Returns |
|--------------|---------|
| `KTDataTable.getInstance(el)` | Existing instance or `null` |
| `KTDataTable.getOrCreateInstance(el, config?)` | Existing or new instance |
| `KTDataTable.init()` | Scans DOM, creates instances |
---
## 4. Column Config
```ts
columns: {
name: {
title: 'Full Name',
render: (item, data, ctx) => `${item}`,
sortType: 'string', // or 'numeric'
sortValue: (cellValue, rowData) => rowData.firstName + ' ' + rowData.lastName,
createdCell: (cell, cellData, rowData, row) => {
cell.classList.add('text-primary');
},
},
actions: {
checkbox: true,
},
}
```
---
## 5. Remote Data Provider
### Response shape
```ts
interface KTDataTableResponseDataInterface {
data: KTDataTableDataInterface[];
totalCount: number;
}
```
### Config
```ts
{
apiEndpoint: 'https://api.example.com/users',
requestMethod: 'POST',
requestHeaders: { 'Authorization': 'Bearer ...' },
mapResponse: (response) => ({ data: response.items, totalCount: response.total }),
mapRequest: (params) => ({ ...params, page: params.page + 1 }),
}
```
---
## 6. Fixed Column Widths
Use `tableLayout: 'fixed'` with `` for consistent column widths across pages:
```html
```
---
## 7. Column Filters
Client-side filtering with `setFilter()`:
```ts
dt.setFilter({ column: 'status', type: 'text', value: 'active' });
dt.setFilter({ column: 'price', type: 'numeric', value: { min: 10, max: 100 } });
```
Filter pipeline runs between search and sort.
---
## 8. Empty State
**Default:** When `data.length === 0`, renders a single row with `infoEmpty` text (default: `"No records found"`). Supports HTML.
**Custom via data attribute:**
```html
```
**Pitfall — never hardcode empty state HTML in `
`:** The DataTable reads tbody rows as "data" on init. Hardcoded content gets treated as 1 row and disappears on re-render. Always use `data-kt-datatable-info-empty` or the JS `infoEmpty` config.
---
## 9. Event System
All events dispatch through dual channel:
1. **Internal callbacks** (`.on()`) — bare name
2. **DOM CustomEvents** (`addEventListener`) — namespaced as `kt.datatable.`
| Event | Payload | When |
|-------|---------|------|
| `update` | — | Data changed. Call `getState()` for details. |
| `sort` | `{ field, order }` | Column header click |
| `change` | `{ cancel }` | Before checkbox toggle (cancelable) |
| `changed` | — | After checkbox change. Call `getChecked()`. |
| `checked` | `{ value }` | Row checked |
| `unchecked` | `{ value }` | Row unchecked |
| `fetchError` | `{ response, error, status, statusText }` | Remote JSON parse failure |
| `error` | `{ error }` | Network fetch failure |
```ts
// Internal callback
dt.on('sort', (payload) => { console.log(payload.field); });
// DOM CustomEvent
el.addEventListener('kt.datatable.sort', (e) => {
console.log(e.detail.payload.field);
});
```
---
## 10. Architecture
Source: `src/components/datatable/`
| File | Purpose |
|------|---------|
| `datatable.ts` | Main class — constructor, `_updateData()`, `_draw()`, `_finalize()`, `_dispose()` |
| `datatable-local-provider.ts` | Local mode data fetch with checksum-based DOM invalidation |
| `datatable-remote-provider.ts` | Remote API mode with AbortController |
| `datatable-state-store.ts` | State management — `patchState()`, `setPage()`, `setSort()` |
| `datatable-checkbox.ts` | Checkbox handler — header check, row check, `reapplyCheckedStates()` |
| `datatable-table-renderer.ts` | Renders data rows into `` |
| `datatable-pagination-renderer.ts` | Renders pagination buttons, page size selector |
| `datatable-sort.ts` | Column sort handler with AbortController-based cleanup |
| `datatable-defaults.ts` | `DATATABLE_DEFAULTS` — static config constants |
| `datatable-utils.ts` | Shared `stripHtml()` utility |
| `datatable-search-handler.ts` | Debounced search input handler |
| `datatable-state-persistence.ts` | localStorage save/load with try/catch guards |
| `datatable-registry.ts` | Instance Map + DOM fallback |
| `datatable-layout-plugin.ts` | Sticky header/locked columns plugin |
| `datatable-spinner.ts` | Loading spinner show/hide |
| `datatable-contracts.ts` | Interfaces and type contracts |
| `datatable-column-utils.ts` | Column resolution utilities |
| `types.ts` | Type definitions (config, state, data interfaces) |
| `index.ts` | Barrel exports |
### Lifecycle (Critical)
Every data update follows this sequence:
```
_updateData()
├── fetchSync() or fetch() ← reads from DOM or API
├── _draw()
│ ├── _cleanupForRedraw() ← cleans listeners + DOM artifacts (NOT registry)
│ ├── _updateTable() ← re-renders tbody rows
│ ├── _updatePagination() ← re-renders pagination buttons
│ ├── afterDraw layoutPlugin hook
│ └── _saveState() ← persists to localStorage
├── _finalize()
│ ├── _checkbox.init() ← re-queries DOM, calls reapplyCheckedStates()
│ ├── _sortHandler.initSort()
│ ├── searchHandler.attach()
│ ├── KTComponents.init()
│ ├── spinner.hide()
│ └── update _contentChecksum ← MUST be last (after DOM modifications)
└── emit 'update'
```
**ORDER MATTERS.** `_finalize()` modifies the DOM. Any checksum saved before `_finalize()` will be stale.
---
## 11. Pitfalls
| Problem | Cause | Fix |
|---------|-------|-----|
| Empty pagination after checkbox select | `_contentChecksum` saved before `_finalize()` | Save checksum at END of `_finalize()` |
| Corrupted localStorage persists | State shrank `originalData` to current page | `localStorage.removeItem('kt_datatable_')` |
| Sort listener leak | `cloneNode(true)` destroys th attrs | Uses AbortController pattern internally |
| Filter stored but never applied | `setFilter()` writes to state but `fetchSync()` ignores it | Filter pipeline runs between search and sort |
| Hardcoded empty state in `` | DataTable reads tbody rows as "data" on init | Use `data-kt-datatable-info-empty` attribute instead |
| Column widths shift on pagination | `table-layout: auto` recalculates per page | Use `table-layout: fixed` with `` |
| Checkbox events invisible to `addEventListener` | Was only internal callbacks | Now dispatches as `kt.datatable.*` CustomEvents |
| Parallel refactoring on shared repo fails | Tight coupling between files | Execute sequentially on single branch |
---
## 12. Build & Testing
```bash
cd ~/Sites/keenthemes/ktui/ktui
npx vitest run src/components/datatable # all pass
npx tsc --noEmit # typecheck
npm run build:webpack # full bundle
npm run build:lib # ESM + CJS library builds
```
**Do NOT edit `lib/esm/` or `lib/cjs/` directly** — they're generated from `npm run build:lib`.
---
## 13. Documentation
- **DataTable docs:** [ktui.io/docs/datatable](https://ktui.io/docs/datatable)
- **Changelog:** [ktui.io/docs/changelog](https://ktui.io/docs/changelog)