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
Post a Comment