Angular Interview Task: Reusable Server-Side Data Table Component
🏆 Angular Interview Task: Reusable Server-Side Data Table Component
This reusable <app-data-table> component supports:
- ✅ Server-side Pagination
- ✅ Server-side Sorting
- ✅ Server-side Search
- ✅ Server-side Filtering (multiple dynamic filters)
- ✅ Config-driven & reusable
- ✅ Works with Angular 17 (Standalone or Module)
- ✅ Clean, interview-ready code
1️⃣ Step 1: Create Models
table-column.model.ts
export interface TableColumn<T> {
key: keyof T | string;
header: string;
sortable?: boolean;
width?: string;
type?: 'text' | 'date' | 'currency' | 'status';
}
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;
}
2️⃣ Step 2: Create Reusable Table Component
Generate standalone component:
ng generate component shared/data-table --standalone
data-table.component.ts
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TableColumn } from './table-column.model';
import { TableRequest } from './table-request.model';
import { TableResponse } from './table-response.model';
@Component({
selector: 'app-data-table',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './data-table.component.html'
})
export class DataTableComponent<T> {
@Input() columns: TableColumn<T>[] = [];
@Input() data: T[] = [];
@Input() totalCount = 0;
@Input() pageSizeOptions: number[] = [5, 10, 20, 50];
@Input() loading = false;
@Output() requestChange = new EventEmitter<TableRequest>();
request: TableRequest = {
pageNumber: 1,
pageSize: 10,
search: '',
sortBy: '',
sortDir: 'asc',
filters: {}
};
get totalPages(): number {
return Math.ceil(this.totalCount / this.request.pageSize);
}
onSearchChange() {
this.request.pageNumber = 1;
this.emitRequest();
}
sort(col: TableColumn<T>) {
if (!col.sortable) return;
const key = col.key.toString();
if (this.request.sortBy === key) {
this.request.sortDir = this.request.sortDir === 'asc' ? 'desc' : 'asc';
} else {
this.request.sortBy = key;
this.request.sortDir = 'asc';
}
this.emitRequest();
}
changePage(page: number) {
if (page < 1 || page > this.totalPages) return;
this.request.pageNumber = page;
this.emitRequest();
}
changePageSize() {
this.request.pageNumber = 1;
this.emitRequest();
}
setFilter(key: string, value: any) {
this.request.filters = { ...this.request.filters, [key]: value };
this.request.pageNumber = 1;
this.emitRequest();
}
clearFilters() {
this.request.filters = {};
this.request.pageNumber = 1;
this.emitRequest();
}
private emitRequest() {
this.requestChange.emit({ ...this.request });
}
getValue(row: any, key: any) {
return row[key];
}
}
data-table.component.html
<div style="padding:10px; border:1px solid #ddd; border-radius:10px;">
<!-- Search + Page Size + Filters -->
<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">
<input
[(ngModel)]="request.search"
(ngModelChange)="onSearchChange()"
placeholder="Search..."
style="padding:8px; width:250px;"
/>
<select [(ngModel)]="request.pageSize" (change)="changePageSize()">
<option *ngFor="let s of pageSizeOptions" [value]="s">{{ s }}</option>
</select>
<button (click)="clearFilters()">Clear Filters</button>
<span *ngIf="loading">Loading...</span>
</div>
<!-- Table -->
<table width="100%" border="1" cellspacing="0" cellpadding="8">
<thead>
<tr>
<th *ngFor="let col of columns" (click)="sort(col)" style="cursor:pointer;">
{{ col.header }}
<span *ngIf="request.sortBy === col.key.toString()">
{{ request.sortDir === 'asc' ? '⬆️' : '⬇️' }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data">
<td *ngFor="let col of columns">{{ getValue(row, col.key) }}</td>
</tr>
<tr *ngIf="!loading && data.length === 0">
<td [attr.colspan]="columns.length" style="text-align:center;">No records found</td>
</tr>
</tbody>
</table>
<!-- Pagination -->
<div style="display:flex; gap:10px; margin-top:10px; align-items:center;">
<button (click)="changePage(request.pageNumber - 1)">Prev</button>
<span>Page {{ request.pageNumber }} of {{ totalPages }}</span>
<button (click)="changePage(request.pageNumber + 1)">Next</button>
</div>
</div>
3️⃣ Step 3: Use Table Component in Parent
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 { DataTableComponent } from '../shared/data-table/data-table.component';
import { TableColumn } from '../shared/data-table/table-column.model';
import { TableRequest } from '../shared/data-table/table-request.model';
import { Invoice } from './invoice.model';
import { InvoiceService } from './invoice.service';
@Component({
selector: 'app-invoice-list',
standalone: true,
imports: [DataTableComponent],
template: `
<h2>Invoices</h2>
<button (click)="applyStatusFilter('Paid')">Paid</button>
<button (click)="applyStatusFilter('Pending')">Pending</button>
<app-data-table
[columns]="columns"
[data]="data"
[totalCount]="totalCount"
[loading]="loading"
(requestChange)="loadInvoices($event)"
></app-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;
lastRequest!: TableRequest;
constructor(private invoiceService: InvoiceService) {}
ngOnInit() {
this.loadInvoices({ pageNumber: 1, pageSize: 10 });
}
loadInvoices(req: TableRequest) {
this.lastRequest = req;
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)
});
}
applyStatusFilter(status: string) {
const req = { ...this.lastRequest };
req.filters = { ...req.filters, status };
req.pageNumber = 1;
this.loadInvoices(req);
}
}
4️⃣ Step 4: Invoice Service (Server-Side API)
invoice.service.ts
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { TableRequest } from '../shared/data-table/table-request.model';
import { TableResponse } from '../shared/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 => {
if (req.filters![key] !== null && req.filters![key] !== undefined) {
params = params.set(key, req.filters![key]);
}
});
}
return this.http.get<TableResponse<Invoice>>(this.baseUrl, { params });
}
}
5️⃣ Step 5: Backend API Example (.NET Core)
Request Example:
GET /api/invoices?pageNumber=1&pageSize=10&search=abc&sortBy=amount&sortDir=desc&status=Paid
Response Example:
{
"data": [],
"totalCount": 120
}
✅ Key Points
- Fully reusable and config-driven
- UI-independent; can use with any model (Invoices, Projects, Users, Orders, etc.)
- Server-side sorting/pagination/search/filtering is scalable
- Filters are dynamic and flexible
- Interview-ready clean architecture
Comments
Post a Comment