Angular Material PRO Reusable Table Component
Angular Material PRO Reusable Table Component
This reusable table supports:
- ✅ MatTable
- ✅ MatPaginator (Server-side)
- ✅ MatSort (Server-side)
- ✅ Search with debounce (RxJS)
- ✅ Dynamic Columns
- ✅ Server-side filtering
- ✅ Reusable for any module (Invoices / Users / Projects / Orders)
1️⃣ Install Angular Material
ng add @angular/material
2️⃣ Create Reusable Table Component
ng g component shared/material-data-table --standalone
3️⃣ Table Models (Same as before)
table-column.model.ts
export interface TableColumn<T> {
key: keyof T | string;
header: string;
sortable?: boolean;
}
table-request.model.ts
export interface TableRequest {
pageNumber: number;
pageSize: number;
search?: string;
sortBy?: string;
sortDir?: 'asc' | 'desc';
filters?: Record<string, any>;
}
table-response.model.ts
export interface TableResponse<T> {
data: T[];
totalCount: number;
}
4️⃣ Angular Material Data Table Component (Reusable)
material-data-table.component.ts
import { CommonModule } from '@angular/common';
import { Component, Input, Output, EventEmitter, ViewChild, AfterViewInit } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatTableModule } from '@angular/material/table';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { TableColumn } from './table-column.model';
import { TableRequest } from './table-request.model';
@Component({
selector: 'app-material-data-table',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatProgressSpinnerModule
],
templateUrl: './material-data-table.component.html'
})
export class MaterialDataTableComponent<T> implements AfterViewInit {
@Input() columns: TableColumn<T>[] = [];
@Input() data: T[] = [];
@Input() totalCount = 0;
@Input() loading = false;
@Input() pageSizeOptions: number[] = [5, 10, 20, 50];
@Output() requestChange = new EventEmitter<TableRequest>();
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
searchControl = new FormControl('');
request: TableRequest = { pageNumber: 1, pageSize: 10, search: '', sortBy: '', sortDir: 'asc', filters: {} };
get displayedColumns(): string[] {
return this.columns.map(c => c.key.toString());
}
ngAfterViewInit(): void {
// 🔥 Search with debounce
this.searchControl.valueChanges
.pipe(debounceTime(400), distinctUntilChanged())
.subscribe(value => {
this.request.search = value ?? '';
this.request.pageNumber = 1;
this.paginator.firstPage();
this.emitRequest();
});
// 🔥 Sorting
this.sort.sortChange.subscribe(s => {
this.request.sortBy = s.active;
this.request.sortDir = (s.direction || 'asc') as any;
this.request.pageNumber = 1;
this.paginator.firstPage();
this.emitRequest();
});
// 🔥 Pagination
this.paginator.page.subscribe(p => {
this.request.pageNumber = p.pageIndex + 1;
this.request.pageSize = p.pageSize;
this.emitRequest();
});
// initial load
this.emitRequest();
}
setFilter(key: string, value: any) {
this.request.filters = { ...this.request.filters, [key]: value };
this.request.pageNumber = 1;
this.paginator.firstPage();
this.emitRequest();
}
clearFilters() {
this.request.filters = {};
this.request.pageNumber = 1;
this.paginator.firstPage();
this.emitRequest();
}
private emitRequest() {
this.requestChange.emit({ ...this.request });
}
getValue(row: any, key: string) {
return row[key];
}
}
material-data-table.component.html
<div style="padding: 12px; border-radius: 12px; border: 1px solid #ddd;">
<!-- Search + Loader -->
<div style="display:flex; gap:10px; align-items:center; margin-bottom:12px;">
<mat-form-field appearance="outline" style="width: 300px;">
<mat-label>Search</mat-label>
<input matInput [formControl]="searchControl" placeholder="Type to search..." />
</mat-form-field>
<button mat-raised-button color="warn" (click)="clearFilters()">Clear Filters</button>
<div *ngIf="loading" style="margin-left:auto;">
<mat-spinner diameter="30"></mat-spinner>
</div>
</div>
<!-- Table -->
<table mat-table [dataSource]="data" matSort>
<ng-container *ngFor="let col of columns" [matColumnDef]="col.key.toString()">
<th mat-header-cell *matHeaderCellDef>
<span *ngIf="col.sortable" mat-sort-header="{{ col.key.toString() }}">{{ col.header }}</span>
<span *ngIf="!col.sortable">{{ col.header }}</span>
</th>
<td mat-cell *matCellDef="let row">{{ getValue(row, col.key.toString()) }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div *ngIf="!loading && data.length === 0" style="text-align:center; padding:15px;">
No records found
</div>
<mat-paginator
[length]="totalCount"
[pageSize]="request.pageSize"
[pageSizeOptions]="pageSizeOptions"
showFirstLastButtons>
</mat-paginator>
</div>
5️⃣ Parent Component Example (Invoices)
invoice.model.ts
export interface Invoice {
id: number;
invoiceNo: string;
projectName: string;
amount: number;
status: string;
}
invoice-list.component.ts
import { Component } from '@angular/core';
import { MaterialDataTableComponent } from '../shared/material-data-table/material-data-table.component';
import { TableColumn } from '../shared/material-data-table/table-column.model';
import { TableRequest } from '../shared/material-data-table/table-request.model';
import { InvoiceService } from './invoice.service';
import { Invoice } from './invoice.model';
@Component({
selector: 'app-invoice-list',
standalone: true,
imports: [MaterialDataTableComponent],
template: `
<h2>Invoices</h2>
<button (click)="filterPaid()">Paid</button>
<button (click)="filterPending()">Pending</button>
<app-material-data-table
[columns]="columns"
[data]="data"
[totalCount]="totalCount"
[loading]="loading"
(requestChange)="loadInvoices($event)">
</app-material-data-table>
`
})
export class InvoiceListComponent {
columns: TableColumn<Invoice>[] = [
{ key: 'invoiceNo', header: 'Invoice No', sortable: true },
{ key: 'projectName', header: 'Project', sortable: true },
{ key: 'amount', header: 'Amount', sortable: true },
{ key: 'status', header: 'Status', sortable: true }
];
data: Invoice[] = [];
totalCount = 0;
loading = false;
constructor(private invoiceService: InvoiceService) {}
loadInvoices(req: TableRequest) {
this.loading = true;
this.invoiceService.getInvoices(req).subscribe({
next: (res) => { this.data = res.data; this.totalCount = res.totalCount; this.loading = false; },
error: () => (this.loading = false)
});
}
filterPaid() {}
filterPending() {}
}
6️⃣ Invoice Service (Server Side)
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { TableRequest } from '../shared/material-data-table/table-request.model';
import { TableResponse } from '../shared/material-data-table/table-response.model';
import { Invoice } from './invoice.model';
@Injectable({ providedIn: 'root' })
export class InvoiceService {
private baseUrl = "https://localhost:7219/api/invoices";
constructor(private http: HttpClient) {}
getInvoices(req: TableRequest): Observable<TableResponse<Invoice>> {
let params = new HttpParams()
.set("pageNumber", req.pageNumber)
.set("pageSize", req.pageSize);
if (req.search) params = params.set("search", req.search);
if (req.sortBy) params = params.set("sortBy", req.sortBy);
if (req.sortDir) params = params.set("sortDir", req.sortDir);
if (req.filters) {
Object.keys(req.filters).forEach(key => {
const value = req.filters![key];
if (value !== null && value !== undefined && value !== '') {
params = params.set(key, value);
}
});
}
return this.http.get<TableResponse<Invoice>>(this.baseUrl, { params });
}
}
⚡ Important Fix (MatSort Header Issue)
For Angular Material, use *ngIf for sortable headers:
<th mat-header-cell *matHeaderCellDef>
<span *ngIf="col.sortable" mat-sort-header="{{ col.key.toString() }}">{{ col.header }}</span>
<span *ngIf="!col.sortable">{{ col.header }}</span>
</th>
✅ PRO Version: Custom Cell Templates (Actions)
<td mat-cell *matCellDef="let row">
<ng-container *ngIf="col.key !== 'actions'; else actionTpl">
{{ getValue(row, col.key.toString()) }}
</ng-container>
<ng-template #actionTpl>
<button mat-icon-button (click)="edit.emit(row)">
<mat-icon>edit</mat-icon>
</button>
</ng-template>
</td>
✅ Interview Q&A
- Q: Why server-side table?
✅ For large data (50k+) client-side is slow. - Q: How to avoid too many API calls?
✅ UsedebounceTimefor search. - Q: How to reuse for multiple pages?
✅ Column config +requestChangeevent.
Comments
Post a Comment