DataTable Component
An enterprise-grade data table with sorting, filtering, pagination, column management, and virtualization support.
Features
- ✅ Sorting: Multi-column sorting with custom comparators
- ✅ Filtering: Global search + column-specific filters
- ✅ Pagination: Client-side and server-side modes
- ✅ Column Management: Show/hide, reorder, resize
- ✅ Selection: Single, multiple, checkbox selection
- ✅ Export: CSV, Excel, PDF export
- ✅ Virtualization: Efficient rendering for large datasets
- ✅ Responsive: Mobile-friendly with responsive modes
- ✅ Accessibility: ARIA labels, keyboard navigation
- ✅ State Persistence: Save/restore table state
Installation
npm install @tess-ui-library/shared primeng primeicons
Basic Usage
Simple Table
import { Component } from '@angular/core';
import { DataTableComponent } from '@tess-ui-library/shared';
interface User {
id: number;
name: string;
email: string;
role: string;
}
@Component({
selector: 'app-users',
standalone: true,
imports: [DataTableComponent],
template: `
<tess-data-table
[data]="users"
[columns]="columns"
[paginator]="true"
[rows]="10">
</tess-data-table>
`
})
export class UsersComponent {
users: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
// ... more users
];
columns = [
{ field: 'id', header: 'ID', sortable: true },
{ field: 'name', header: 'Name', sortable: true, filterable: true },
{ field: 'email', header: 'Email', filterable: true },
{ field: 'role', header: 'Role', sortable: true }
];
}
With Selection
import { Component, signal } from '@angular/core';
import { DataTableComponent } from '@tess-ui-library/shared';
import { ButtonModule } from 'primeng/button';
@Component({
selector: 'app-users-table',
standalone: true,
imports: [DataTableComponent, ButtonModule],
template: `
<div class="table-actions">
<button pButton
label="Delete Selected"
icon="pi pi-trash"
[disabled]="selectedUsers().length === 0"
(click)="deleteSelected()">
</button>
<span>{{ selectedUsers().length }} selected</span>
</div>
<tess-data-table
[data]="users"
[columns]="columns"
[selection]="selectedUsers()"
(selectionChange)="onSelectionChange($event)"
selectionMode="multiple"
[showCheckbox]="true">
</tess-data-table>
`
})
export class UsersTableComponent {
users = [/* ... */];
columns = [/* ... */];
selectedUsers = signal<User[]>([]);
onSelectionChange(selection: User[]) {
this.selectedUsers.set(selection);
}
deleteSelected() {
const ids = this.selectedUsers().map(u => u.id);
// Call API to delete users
console.log('Deleting users:', ids);
}
}
API Reference
Component Inputs
| Input | Type | Default | Description |
|---|---|---|---|
data | T[] | [] | Array of data objects |
columns | TableColumn[] | [] | Column definitions |
paginator | boolean | false | Enable pagination |
rows | number | 10 | Rows per page |
totalRecords | number | data.length | Total records (server-side) |
lazy | boolean | false | Enable lazy loading (server-side) |
sortMode | 'single' | 'multiple' | 'single' | Sorting mode |
selectionMode | 'single' | 'multiple' | - | Selection mode |
selection | T | T[] | - | Selected item(s) |
showCheckbox | boolean | false | Show selection checkboxes |
globalFilterFields | string[] | - | Fields for global search |
exportFilename | string | 'export' | Filename for exports |
stateKey | string | - | LocalStorage key for state persistence |
responsive | boolean | true | Enable responsive mode |
Component Outputs
| Output | Type | Description |
|---|---|---|
selectionChange | EventEmitter<T | T[]> | Emitted when selection changes |
rowClick | EventEmitter<T> | Emitted when row is clicked |
sort | EventEmitter<SortEvent> | Emitted when sort changes |
filter | EventEmitter<FilterEvent> | Emitted when filter changes |
page | EventEmitter<PageEvent> | Emitted when page changes |
lazyLoad | EventEmitter<LazyLoadEvent> | Emitted for lazy loading |
Column Definition
interface TableColumn {
field: string; // Property name
header: string; // Display header
sortable?: boolean; // Enable sorting
filterable?: boolean; // Enable filtering
filterType?: 'text' | 'number' | 'date' | 'boolean';
width?: string; // Column width (e.g., '200px', '20%')
frozen?: boolean; // Freeze column (left side)
hidden?: boolean; // Hide column
exportable?: boolean; // Include in exports (default: true)
template?: TemplateRef<any>; // Custom cell template
}
Advanced Features
Server-Side Pagination & Filtering
import { Component, signal } from '@angular/core';
import { LazyLoadEvent } from '@tess-ui-library/shared';
@Component({
selector: 'app-users-server',
template: `
<tess-data-table
[data]="users()"
[columns]="columns"
[lazy]="true"
[totalRecords]="totalRecords()"
[rows]="10"
[paginator]="true"
[loading]="loading()"
(lazyLoad)="onLazyLoad($event)">
</tess-data-table>
`
})
export class UsersServerComponent {
users = signal<User[]>([]);
totalRecords = signal(0);
loading = signal(false);
columns = [/* ... */];
async onLazyLoad(event: LazyLoadEvent) {
this.loading.set(true);
const result = await this.userService.getUsers({
page: event.first / event.rows,
pageSize: event.rows,
sortField: event.sortField,
sortOrder: event.sortOrder,
filters: event.filters
});
if (result.success) {
this.users.set(result.value.items);
this.totalRecords.set(result.value.total);
}
this.loading.set(false);
}
}
Custom Cell Templates
import { Component, TemplateRef, ViewChild } from '@angular/core';
@Component({
selector: 'app-users-custom',
template: `
<tess-data-table
[data]="users"
[columns]="columns">
</tess-data-table>
<ng-template #statusTemplate let-row>
<span [class]="'badge badge-' + row.status">
{{ row.status }}
</span>
</ng-template>
<ng-template #actionsTemplate let-row>
<button pButton
icon="pi pi-pencil"
class="p-button-sm"
(click)="edit(row)">
</button>
<button pButton
icon="pi pi-trash"
class="p-button-sm p-button-danger"
(click)="delete(row)">
</button>
</ng-template>
`
})
export class UsersCustomComponent {
@ViewChild('statusTemplate') statusTemplate!: TemplateRef<any>;
@ViewChild('actionsTemplate') actionsTemplate!: TemplateRef<any>;
users = [/* ... */];
columns = [
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name' },
{
field: 'status',
header: 'Status',
template: this.statusTemplate
},
{
field: 'actions',
header: 'Actions',
template: this.actionsTemplate,
exportable: false
}
];
edit(user: User) { /* ... */ }
delete(user: User) { /* ... */ }
}
Export Data
import { Component, ViewChild } from '@angular/core';
import { DataTableComponent } from '@tess-ui-library/shared';
@Component({
selector: 'app-users-export',
template: `
<div class="export-buttons">
<button pButton
label="CSV"
icon="pi pi-file"
(click)="exportCSV()">
</button>
<button pButton
label="Excel"
icon="pi pi-file-excel"
(click)="exportExcel()">
</button>
<button pButton
label="PDF"
icon="pi pi-file-pdf"
(click)="exportPDF()">
</button>
</div>
<tess-data-table
#dt
[data]="users"
[columns]="columns"
exportFilename="users">
</tess-data-table>
`
})
export class UsersExportComponent {
@ViewChild('dt') table!: DataTableComponent;
users = [/* ... */];
columns = [/* ... */];
exportCSV() {
this.table.exportCSV();
}
exportExcel() {
this.table.exportExcel();
}
exportPDF() {
this.table.exportPDF();
}
}
State Persistence
@Component({
selector: 'app-users-state',
template: `
<tess-data-table
[data]="users"
[columns]="columns"
[paginator]="true"
stateKey="users-table-state">
<!-- Table automatically saves/restores:
- Sort order
- Filter values
- Page number
- Column visibility
- Column order
-->
</tess-data-table>
`
})
export class UsersStateComponent {
// State persisted to localStorage with key "users-table-state"
}
Responsive Modes
Stack Mode (Mobile)
@Component({
template: `
<tess-data-table
[data]="users"
[columns]="columns"
[responsive]="true"
responsiveLayout="stack">
<!-- On mobile, displays as stacked cards -->
</tess-data-table>
`
})
Scroll Mode (Tablet/Desktop)
@Component({
template: `
<tess-data-table
[data]="users"
[columns]="columns"
[responsive]="true"
responsiveLayout="scroll">
<!-- Horizontal scroll on small screens -->
</tess-data-table>
`
})
Examples
StackBlitz Demo
Try the interactive example:
Best Practices
1. Use Lazy Loading for Large Datasets
// ✅ GOOD: Lazy load for 1000+ records
<tess-data-table
[lazy]="true"
[totalRecords]="10000"
(lazyLoad)="loadData($event)">
</tess-data-table>
// ❌ BAD: Client-side for large datasets
<tess-data-table [data]="tenThousandRecords">
</tess-data-table>
2. Define Column Widths
// ✅ GOOD: Explicit widths
columns = [
{ field: 'id', header: 'ID', width: '80px' },
{ field: 'name', header: 'Name', width: '200px' },
{ field: 'email', header: 'Email', width: '250px' }
];
// ❌ BAD: No widths (may cause layout issues)
columns = [
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name' }
];
3. Optimize Filter Performance
// ✅ GOOD: Debounce filter input
<input
pInputText
(input)="onFilterChange($event)"
[debounce]="300">
// Filter handler
onFilterChange(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.table.filterGlobal(value, 'contains');
}
Testing
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DataTableComponent } from '@tess-ui-library/shared';
describe('DataTableComponent', () => {
let component: DataTableComponent;
let fixture: ComponentFixture<DataTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DataTableComponent]
}).compileComponents();
fixture = TestBed.createComponent(DataTableComponent);
component = fixture.componentInstance;
});
it('should render rows', () => {
component.data = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
component.columns = [
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name' }
];
fixture.detectChanges();
const rows = fixture.nativeElement.querySelectorAll('tr');
expect(rows.length).toBe(3); // Header + 2 data rows
});
it('should emit selection change', () => {
const spy = jest.spyOn(component.selectionChange, 'emit');
const user = { id: 1, name: 'John' };
component.onRowSelect(user);
expect(spy).toHaveBeenCalledWith([user]);
});
it('should sort data', () => {
component.data = [
{ id: 2, name: 'Jane' },
{ id: 1, name: 'John' }
];
component.columns = [
{ field: 'id', header: 'ID', sortable: true }
];
component.sort({ field: 'id', order: 1 });
expect(component.data[0].id).toBe(1);
expect(component.data[1].id).toBe(2);
});
});
Troubleshooting
Performance Issues
Problem: Table is slow with large datasets.
Solutions:
- Enable
[lazy]="true"for server-side pagination - Use
[virtualScroll]="true"for client-side large datasets - Disable sorting/filtering on heavy columns
Columns Not Resizing
Problem: Columns don't resize properly.
Solution: Set explicit widths:
columns = [
{ field: 'id', header: 'ID', width: '100px' },
{ field: 'name', header: 'Name', width: '200px' }
];
Export Not Working
Problem: Export buttons don't download files.
Solution: Ensure export libraries are installed:
npm install file-saver xlsx jspdf jspdf-autotable