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

Popular posts from this blog

Debouncing & Throttling in RxJS: Optimizing API Calls and User Interactions

Promises in Angular

Comprehensive Guide to C# and .NET Core OOP Concepts and Language Features