rohansahana-dynamic-table
v2.0.4
Published
Dynamic table library workspace (Angular 20) including dynamic-table project
Maintainers
Keywords
Readme
rohansahana-dynamic-table
Zero boilerplate. Maximum power. Drop in a single
<dynamic-table>tag and get sorting, filtering, pagination, virtual scroll, column resize & reorder, row selection, row expansion, CSV export, persisted views & state, full keyboard accessibility, custom theming, and an extensible plugin system — all out of the box.
✨ Why rohansahana-dynamic-table?
| Feature | rohansahana-dynamic-table | ag-Grid (free) | Angular Material Table | ngx-datatable | PrimeNG Table | |---|:---:|:---:|:---:|:---:|:---:| | Standalone (no NgModule) | ✅ | ❌ | ✅ | ❌ | ❌ | | Angular 20+ signals-ready | ✅ | ❌ | ⚠️ | ❌ | ⚠️ | | Virtual scroll (CDK) | ✅ | ✅ | ❌ | ✅ | ✅ | | Column resize + reorder | ✅ | ✅ | ❌ | ✅ | ✅ | | Row resize | ✅ | ❌ | ❌ | ❌ | ❌ | | Saved views / persisted state | ✅ | ❌ | ❌ | ❌ | ❌ | | Plugin system | ✅ | ✅ | ❌ | ❌ | ❌ | | CSV export built-in | ✅ | 💰 | ❌ | ❌ | ✅ | | Adaptive page sizing | ✅ | ❌ | ❌ | ❌ | ❌ | | Server-side mode | ✅ | ✅ | ⚠️ | ✅ | ✅ | | Zero dependencies (beyond Angular) | ✅ | ❌ | ✅ | ❌ | ❌ | | Fully keyboard accessible | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ | | Free & MIT licensed | ✅ | ⚠️ | ✅ | ✅ | ✅ |
Built for Angular 20+. Standalone-first. Signal-ready. Tree-shakable. Zero lock-in.
📦 Install
npm i rohansahana-dynamic-table# or with yarn
yarn add rohansahana-dynamic-table# or with pnpm
pnpm add rohansahana-dynamic-tablePeer dependencies
| Package | Version |
|---|---|
| @angular/core | >= 20 |
| @angular/common | >= 20 |
| @angular/cdk | >= 20 |
| rxjs | >= 7.8 |
Note: If you're already on Angular 20+, you likely have all of these installed.
🚀 Quick start (standalone)
Get a fully functional, production-ready data table in under 30 lines:
import { Component } from '@angular/core';
import { DynamicTableComponent, type DynamicTableColumn } from 'rohansahana-dynamic-table';
interface User {
id: number;
name: string;
role: string;
active: boolean;
created: string;
}
@Component({
selector: 'app-users',
standalone: true,
imports: [DynamicTableComponent],
template: `
<dynamic-table
[columns]="columns"
[data]="rows"
stateKey="users-demo"
[pagination]="{ pageIndex: 0, pageSize: 20 }"
[virtualScroll]="{ enabled: true, itemSize: 36 }"
[globalFilter]="true"
[showFilterRow]="true"
[selectable]="'multi'"
(rowClick)="onRowClick($event)"
(selectionChange)="onSelection($event)"
/>
`,
})
export class UsersComponent {
columns: DynamicTableColumn<User>[] = [
{ key: 'id', header: 'ID', width: 80, sortable: true },
{ key: 'name', header: 'Name', sortable: true, filterable: true },
{ key: 'role', header: 'Role', sortable: true, filterable: true },
{ key: 'active', header: 'Active', width: 100, sortable: true },
{ key: 'created', header: 'Created', sortable: true },
];
rows: User[] = Array.from({ length: 200 }).map((_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
role: ['Admin', 'Manager', 'Viewer'][i % 3]!,
active: i % 2 === 0,
created: new Date(Date.now() - i * 86400000).toISOString().slice(0, 10),
}));
onRowClick(row: User) {
console.log('Row clicked', row);
}
onSelection(ev: any) {
console.log('Selection', ev);
}
}That's it. No modules to import. No complex configuration objects. Just one component, one template tag, and you're live.
📖 Complete Component API
Inputs
Core
| Input | Type | Default | Description |
|---|---|---|---|
| columns (required) | DynamicTableColumn<T>[] | — | Column definitions array |
| data | T[] \| Observable<T[]> | [] | Row data (supports async streams!) |
| rowKey | (row: T) => string | auto-detect id | Stable row identity function |
Pagination
| Input | Type | Default | Description |
|---|---|---|---|
| pagination | PaginationConfig \| false | false | Enable/configure pagination |
| adaptivePageSize | boolean | false | Auto-derive page sizes from container height |
| pageSizeOptions | number[] | [10,20,50,100] | Page size dropdown options |
Sorting & Filtering
| Input | Type | Default | Description |
|---|---|---|---|
| sortMode | 'single' \| 'multi' \| 'none' | 'single' | Sort behavior |
| globalFilter | boolean | false | Show toolbar search input |
| showFilterRow | boolean | false | Show per-column filter inputs |
Selection & Expansion
| Input | Type | Default | Description |
|---|---|---|---|
| selectable | boolean \| 'multi' | false | Row selection mode |
| expandable | boolean | false | Enable row expansion |
Virtual Scroll & Layout
| Input | Type | Default | Description |
|---|---|---|---|
| virtualScroll | boolean \| Partial<VirtualScrollConfig> | false | CDK virtual scroll |
| rowHeight | number | 36 | Row height in px (also used by virtual scroll) |
| height | number \| string | auto | Fixed container height |
| dense | boolean | false | Compact row spacing |
Column Interactions
| Input | Type | Default | Description |
|---|---|---|---|
| enableColumnResize | boolean | false | Drag-to-resize columns |
| enableColumnReorder | boolean | false | Drag-to-reorder columns |
| enableRowResize | boolean | false | Drag handle to resize rows |
| showColumnPanel | boolean | false | Column visibility + saved views panel |
Persistence & Extensibility
| Input | Type | Default | Description |
|---|---|---|---|
| stateKey | string | — | localStorage key for state persistence |
| theme | string | — | Adds dt-theme-${theme} CSS class |
| plugins | DynamicTablePlugin<T>[] | [] | Instance-scoped plugins |
Outputs
| Output | Payload Type | Description |
|---|---|---|
| rowClick | T | Emitted when a row is clicked |
| selectionChange | DynamicTableSelectionChangeEvent<T> | Selection added/removed |
| sortChange | DynamicTableSortChangeEvent | Active sort changed |
| filterChange | DynamicTableFilterChangeEvent | Filter value changed |
| pageChange | DynamicTablePageChangeEvent | Page index or size changed |
| columnResize | DynamicTableColumnResizeEvent | Column width changed |
| columnReorder | DynamicTableColumnReorderEvent | Column order changed |
| rowResize | DynamicTableRowResizeEvent | Row height changed |
| stateChange | DynamicTableState | Full state snapshot changed |
🔧 Column Configuration
Basic
const columns: DynamicTableColumn<User>[] = [
{ key: 'name', header: 'Name', sortable: true, filterable: true },
{ key: 'role', header: 'Role', sortable: true },
];Nested properties with dot-paths
Access deeply nested data effortlessly:
// Row shape: { user: { profile: { name: '...' } } }
{ key: 'user.profile.name', header: 'Name' }Custom cell templates
Render anything inside a cell — components, icons, badges, action buttons:
<ng-template #nameCell let-value="value" let-row="row">
<strong>{{ value }}</strong>
<span style="opacity: .7">({{ row.role }})</span>
</ng-template>
<dynamic-table [columns]="columns" [data]="rows"></dynamic-table>@ViewChild('nameCell', { static: true }) nameCell!: TemplateRef<any>;
ngOnInit() {
this.columns = [
{ key: 'name', header: 'Name', cellTemplate: this.nameCell },
];
}Column-level custom sort & filter
Want the table UI (sort arrows, filter inputs) but handle logic yourself (e.g., API calls)?
{ key: 'name', header: 'Name', sortable: 'custom', filterable: 'custom' }When 'custom' is set, the component skips client-side processing for that column but still emits sortChange / filterChange — giving you full control.
🌐 Client-side vs Server-side Data
Client-side (default)
Out of the box, the table handles everything locally:
- ✅ Global search + column filters
- ✅ Multi-column sorting
- ✅ Pagination slicing
Server-side mode
For large datasets backed by an API:
pagination = { pageIndex: 0, pageSize: 20, serverSide: true, length: 1234 };In this mode the table:
- ❌ Does not apply client-side sorting/filtering/paging
- ✅ Still emits
sortChange,filterChange,pageChangefor you to call your API - ✅ Uses
pagination.lengthas total row count for the pager
// React to changes and fetch from your API
onSortChange(event: DynamicTableSortChangeEvent) {
this.rows = await this.api.getUsers({
sort: event.active,
direction: event.direction,
page: this.page,
size: this.pageSize,
});
}🔑 Row Keys (recommended)
Selection, expansion, and state persistence rely on stable row identity.
Defaults:
- If your row has an
idfield → used automatically - Otherwise → internal index-based key (not stable across data refreshes)
Best practice — always provide rowKey:
<dynamic-table [rowKey]="rowKeyFn" [columns]="columns" [data]="rows" />rowKeyFn = (row: User) => String(row.id);📂 Row Expansion
Show detail panels, nested tables, forms — anything:
<dynamic-table [columns]="columns" [data]="rows" [expandable]="true">
<ng-template #rowExpansion let-row>
<div style="padding: 8px">
<h4>Details for {{ row.name }}</h4>
<pre>{{ row | json }}</pre>
</div>
</ng-template>
</dynamic-table>Tip: Combine with
[selectable]="'multi'"for master-detail patterns.
↕️ Row Resizing
Let users control row density interactively:
<dynamic-table
[enableRowResize]="true"
(rowResize)="onRowResize($event)"
[columns]="columns"
[data]="rows"
/>Row resize automatically updates
rowHeightand the virtual scroll item size when virtual scrolling is enabled.
📤 CSV Export
Built-in, zero-config CSV export:
@ViewChild(DynamicTableComponent) table!: DynamicTableComponent<User>;
download() {
this.table.export({
format: 'csv',
filename: 'users-export',
bom: true, // UTF-8 BOM for Excel compatibility
});
}Exports the currently filtered & sorted data — what you see is what you get.
💾 Persisted State & Saved Views
Set stateKey and the table automatically saves & restores from localStorage:
<dynamic-table stateKey="users-table-v1" [columns]="columns" [data]="rows" />What gets persisted:
- ✅ Column order, widths, visibility
- ✅ Sort stack (multi-sort supported)
- ✅ Filter values + global search term
- ✅ Pagination page index & page size
- ✅ Selection keys
Named views: The column panel ([showColumnPanel]="true") lets users save, load, and delete named view presets — all stored under the same stateKey.
🔌 Plugin System
Extend the table without forking. Plugins hook into the data pipeline and lifecycle.
Instance-scoped plugins
<dynamic-table [plugins]="plugins" [columns]="columns" [data]="rows" />import type { DynamicTablePlugin } from 'rohansahana-dynamic-table';
const analyticsPlugin: DynamicTablePlugin<User> = {
id: 'analytics',
afterDataProcess(rows) {
console.debug(`[analytics] ${rows.length} rows rendered`);
},
};
plugins = [analyticsPlugin];App-wide plugins (via Dependency Injection)
Register plugins globally so every <dynamic-table> in your app benefits:
import { bootstrapApplication } from '@angular/platform-browser';
import { DYNAMIC_TABLE_PLUGINS, type DynamicTablePlugin } from 'rohansahana-dynamic-table';
const globalPlugin: DynamicTablePlugin = {
id: 'global-logger',
afterDataProcess(rows) {
console.log('[global] processed', rows.length, 'rows');
},
};
bootstrapApplication(AppComponent, {
providers: [
{ provide: DYNAMIC_TABLE_PLUGINS, multi: true, useValue: globalPlugin },
],
});Example plugins bundle
Get started with pre-built plugins:
import { loggingPlugin } from 'rohansahana-dynamic-table/samples/example-plugins';🎨 Theming & Customization
CSS custom properties
Override variables on the host element for instant theming:
dynamic-table {
/* Header */
--dynamic-table-header-bg: #1a1a2e;
--dynamic-table-header-color: #e0e0e0;
/* Rows */
--dynamic-table-row-hover-bg: #f0f4ff;
--dynamic-table-row-selected-bg: #e3f2fd;
--dynamic-table-row-stripe-bg: #fafafa;
/* Borders */
--dynamic-table-border-color: #e0e0e0;
--dynamic-table-border-radius: 8px;
/* Typography */
--dynamic-table-font-family: 'Inter', sans-serif;
--dynamic-table-font-size: 14px;
/* Spacing */
--dynamic-table-cell-padding: 8px 12px;
}Named themes
<dynamic-table theme="dark" [columns]="columns" [data]="rows" />This adds the class dt-theme-dark to the host, which you can target in your global styles:
dynamic-table.dt-theme-dark {
--dynamic-table-header-bg: #0d1117;
--dynamic-table-header-color: #c9d1d9;
--dynamic-table-row-hover-bg: #161b22;
}⌨️ Keyboard Accessibility (WCAG 2.1 AA)
Full keyboard navigation out of the box:
| Key | Action |
|---|---|
| ↑ ↓ ← → | Navigate between cells |
| Home / End | Jump to first/last cell in row |
| PageUp / PageDown | Navigate pages (when pagination enabled) |
| Enter | Expand/collapse row (when expandable) |
| Space | Toggle row selection (multi-select mode) |
| Tab | Move through interactive elements |
Screen reader friendly with proper ARIA roles, labels, and live regions.
🧩 Common Recipes
Master-detail with nested table
<dynamic-table [columns]="columns" [data]="rows" [expandable]="true" [selectable]="'multi'">
<ng-template #rowExpansion let-row>
<dynamic-table [columns]="detailColumns" [data]="row.orders" [dense]="true" />
</ng-template>
</dynamic-table>Observable data source
rows$ = this.http.get<User[]>('/api/users');<dynamic-table [columns]="columns" [data]="rows$" />Inline action buttons
<ng-template #actions let-row="row">
<button (click)="edit(row)">✏️</button>
<button (click)="delete(row)">🗑️</button>
</ng-template>{ key: 'actions', header: '', cellTemplate: this.actions, width: 100, sortable: false }🏗️ Framework Compatibility
| Angular Version | Supported | |---|---| | 20.x | ✅ Fully supported | | 21.x+ | ✅ Forward compatible | | 19.x and below | ❌ Use Angular 20+ |
📊 Performance
- Virtual scroll — render 100,000+ rows at 60fps using Angular CDK
- OnPush change detection — minimal re-renders
- Tree-shakable — only ship what you use
- Lazy template instantiation — expansion templates created on demand
- Tiny footprint — < 15KB gzipped with zero external dependencies
📄 License
MIT © Rohan Sahana
