npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rohansahana-dynamic-table

v2.0.4

Published

Dynamic table library workspace (Angular 20) including dynamic-table project

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-table

Peer 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, pageChange for you to call your API
  • ✅ Uses pagination.length as 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 id field → 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 rowHeight and 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