Skip to main content

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

InputTypeDefaultDescription
dataT[][]Array of data objects
columnsTableColumn[][]Column definitions
paginatorbooleanfalseEnable pagination
rowsnumber10Rows per page
totalRecordsnumberdata.lengthTotal records (server-side)
lazybooleanfalseEnable lazy loading (server-side)
sortMode'single' | 'multiple''single'Sorting mode
selectionMode'single' | 'multiple'-Selection mode
selectionT | T[]-Selected item(s)
showCheckboxbooleanfalseShow selection checkboxes
globalFilterFieldsstring[]-Fields for global search
exportFilenamestring'export'Filename for exports
stateKeystring-LocalStorage key for state persistence
responsivebooleantrueEnable responsive mode

Component Outputs

OutputTypeDescription
selectionChangeEventEmitter<T | T[]>Emitted when selection changes
rowClickEventEmitter<T>Emitted when row is clicked
sortEventEmitter<SortEvent>Emitted when sort changes
filterEventEmitter<FilterEvent>Emitted when filter changes
pageEventEmitter<PageEvent>Emitted when page changes
lazyLoadEventEmitter<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:

Open in StackBlitz →

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