Enterprise Final++ Angular Table Architecture

Enterprise Final++ Angular Table Architecture

This guide demonstrates a fully reusable Angular Material table setup with server-side paging, sorting, filtering, selection, export, and URL query parameter sync. It's designed for large enterprise applications.


📁 Folder Structure

shared/
  components/
    enterprise-table/
      enterprise-table.component.ts
      enterprise-table.component.html
      enterprise-table.models.ts

core/
  services/
    api-client.service.ts
  interceptors/
    error.interceptor.ts

features/
  users/
    users.component.ts
    users.component.html
    users.service.ts
    users.models.ts

1️⃣ Models (Enterprise Table Contracts)


// enterprise-table.models.ts
import { SortDirection } from "@angular/material/sort";
import { TemplateRef } from "@angular/core";

export interface TableColumn<T> {
  key: keyof T | string;
  header: string;
  sortable?: boolean;

  type?: "text" | "date" | "number" | "currency" | "status";
  cellTemplate?: TemplateRef<any>;
}

export interface TableQuery {
  pageIndex: number;
  pageSize: number;
  search: string;
  sortBy: string;
  sortDir: SortDirection;
  filters: Record<string, any>;
}

export interface PagedResponse<T> {
  items: T[];
  totalCount: number;
}

2️⃣ Enterprise Table Component (Ultimate Reusable)

Generic table supporting selection, export, and debounced search.


// enterprise-table.component.ts
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy } from "@angular/core";
import { FormControl } from "@angular/forms";
import { PageEvent } from "@angular/material/paginator";
import { Sort } from "@angular/material/sort";
import { SelectionModel } from "@angular/cdk/collections";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged, takeUntil } from "rxjs/operators";
import { TableColumn, TableQuery } from "./enterprise-table.models";

@Component({
  selector: "app-enterprise-table",
  templateUrl: "./enterprise-table.component.html",
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EnterpriseTableComponent<T> implements OnInit, OnDestroy {
  @Input() title = "Enterprise Table";
  @Input() columns: TableColumn<T>[] = [];
  @Input() data: T[] = [];
  @Input() totalCount = 0;
  @Input() loading = false;
  @Input() errorMessage = "";

  @Input() enableSelection = true;
  @Input() enableExport = true;

  @Input() initialQuery: TableQuery = {
    pageIndex: 0,
    pageSize: 10,
    search: "",
    sortBy: "",
    sortDir: "",
    filters: {},
  };

  @Output() queryChange = new EventEmitter<TableQuery>();
  @Output() selectedRowsChange = new EventEmitter<T[]>();
  @Output() exportCsv = new EventEmitter<void>();

  query: TableQuery = { ...this.initialQuery };
  searchControl = new FormControl("");
  selection = new SelectionModel<T>(true, []);
  private destroy$ = new Subject<void>();

  ngOnInit(): void {
    this.searchControl.setValue(this.query.search);

    this.searchControl.valueChanges
      .pipe(debounceTime(400), distinctUntilChanged(), takeUntil(this.destroy$))
      .subscribe(val => {
        this.query = { ...this.query, search: val ?? "", pageIndex: 0 };
        this.queryChange.emit(this.query);
      });

    this.queryChange.emit(this.query);
  }

  get displayedColumns(): string[] {
    const cols = this.columns.map(c => c.key as string);
    return this.enableSelection ? ["select", ...cols] : cols;
  }

  toggleAllRows() {
    if (this.isAllSelected()) this.selection.clear();
    else this.data.forEach(row => this.selection.select(row));
    this.selectedRowsChange.emit(this.selection.selected);
  }

  toggleRow(row: T) {
    this.selection.toggle(row);
    this.selectedRowsChange.emit(this.selection.selected);
  }

  isAllSelected() {
    return this.data.length > 0 && this.selection.selected.length === this.data.length;
  }

  onPageChange(event: PageEvent) {
    this.query = { ...this.query, pageIndex: event.pageIndex, pageSize: event.pageSize };
    this.queryChange.emit(this.query);
  }

  onSortChange(sort: Sort) {
    this.query = { ...this.query, sortBy: sort.active, sortDir: sort.direction, pageIndex: 0 };
    this.queryChange.emit(this.query);
  }

  applyFilter(key: string, value: any) {
    this.query = { ...this.query, filters: { ...this.query.filters, [key]: value }, pageIndex: 0 };
    this.queryChange.emit(this.query);
  }

  clearFilters() {
    this.query = { ...this.query, filters: {}, pageIndex: 0 };
    this.queryChange.emit(this.query);
  }

  onExport() { this.exportCsv.emit(); }

  ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); }
}

3️⃣ Enterprise Table HTML Template


// enterprise-table.component.html
<div class="d-flex justify-content-between align-items-center mb-3"> <h2 class="m-0">{{ title }}</h2> <div class="d-flex gap-2 align-items-center"> <mat-form-field appearance="outline" style="width: 320px;"> <mat-label>Search</mat-label> <input matInput [formControl]="searchControl" placeholder="Search..." /> <button mat-icon-button matSuffix *ngIf="searchControl.value" (click)="searchControl.setValue('')"> <mat-icon>close</mat-icon> </button> </mat-form-field> <button mat-raised-button color="primary" *ngIf="enableExport" (click)="onExport()"> Export CSV </button> </div> </div> <div *ngIf="enableSelection && selection.selected.length > 0" class="mb-2"> <mat-chip color="primary" selected>Selected: {{ selection.selected.length }}</mat-chip> </div> <mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar> <div *ngIf="!loading && errorMessage" class="p-2 mb-2" style="background:#ffe6e6;border-radius:8px;"> {{ errorMessage }} </div> <table mat-table [dataSource]="data" matSort (matSortChange)="onSortChange($event)" class="w-100"> <!-- SELECT --> <ng-container matColumnDef="select" *ngIf="enableSelection"> <th mat-header-cell *matHeaderCellDef> <mat-checkbox (change)="toggleAllRows()" [checked]="isAllSelected()" [indeterminate]="selection.selected.length > 0 && !isAllSelected()"></mat-checkbox> </th> <td mat-cell *matCellDef="let row"> <mat-checkbox (change)="toggleRow(row)" [checked]="selection.isSelected(row)"></mat-checkbox> </td> </ng-container> <!-- DYNAMIC COLUMNS --> <ng-container *ngFor="let col of columns" [matColumnDef]="col.key as string"> <th mat-header-cell *matHeaderCellDef mat-sort-header [disabled]="!col.sortable">{{ col.header }}</th> <td mat-cell *matCellDef="let row"> <ng-container *ngIf="col.cellTemplate; else defaultCell"> <ng-container *ngTemplateOutlet="col.cellTemplate; context: { $implicit: row }"></ng-container> </ng-container> <ng-template #defaultCell>{{ row[col.key as keyof typeof row] }}</ng-template> </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> <tr *ngIf="!loading && data.length === 0"> <td [attr.colspan]="displayedColumns.length" class="text-center p-4">No records found.</td> </tr> </table> <mat-paginator [length]="totalCount" [pageIndex]="query.pageIndex" [pageSize]="query.pageSize" [pageSizeOptions]="[5,10,20,50,100]" (page)="onPageChange($event)"></mat-paginator>

4️⃣ Users Feature with URL Query Param Sync

This component reads/writes the table state to the URL—so deep linking and refreshes maintain table state.


// users.component.ts
...
// See previous code snippet in the architecture

5️⃣ Users HTML


<app-enterprise-table
  title="Users (Enterprise Final++)"
  [columns]="columns"
  [data]="data"
  [totalCount]="totalCount"
  [loading]="loading"
  [errorMessage]="errorMessage"
  [enableSelection]="true"
  [enableExport]="true"
  (queryChange)="onQueryChange($event)"
  (selectedRowsChange)="onSelectedRowsChange($event)"
  (exportCsv)="exportCsv()">
</app-enterprise-table>

6️⃣ Key Enterprise Features

  • ✅ Generic reusable table (<T>)
  • ✅ Server-side paging, sorting, filtering
  • ✅ Debounced search with cancellation of old requests
  • ✅ Selection & bulk actions
  • ✅ Export CSV
  • ✅ URL state sync for deep linking
  • ✅ Strong typing with TypeScript
  • ✅ Error handling with user feedback

💡 This setup is “Final++” because it includes everything an enterprise application expects from a data table.

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