Enterprise Ultimate Angular Material Table (Reusable)
Enterprise Ultimate Angular Material Table (Reusable)
✅ Folder Structure (Enterprise)
src/app/
shared/
components/
pro-table/
pro-table.component.ts
pro-table.component.html
pro-table.component.scss
pro-table.models.ts
features/
users/
users.component.ts
users.component.html
users.service.ts
users.models.ts
1) ✅ Pro Table Models (Generic + Strong Types)
import { SortDirection } from "@angular/material/sort";
export interface TableColumn<T> {
key: keyof T | string;
header: string;
sortable?: boolean;
type?: "text" | "date" | "number" | "currency" | "status";
}
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) ✅ Pro Table Component (Enterprise Logic)
import {
Component,
Input,
Output,
EventEmitter,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
} from "@angular/core";
import { FormControl } from "@angular/forms";
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged, takeUntil } from "rxjs/operators";
import { PageEvent } from "@angular/material/paginator";
import { Sort } from "@angular/material/sort";
import { TableColumn, TableQuery } from "./pro-table.models";
@Component({
selector: "app-pro-table",
templateUrl: "./pro-table.component.html",
styleUrls: ["./pro-table.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProTableComponent<T> implements OnInit, OnDestroy {
@Input() title = "Enterprise Table";
@Input() columns: TableColumn<T>[] = [];
@Input() data: T[] = [];
@Input() totalCount = 0;
@Input() loading = false;
@Input() initialQuery: TableQuery = {
pageIndex: 0,
pageSize: 10,
search: "",
sortBy: "",
sortDir: "",
filters: {},
};
@Output() queryChange = new EventEmitter<TableQuery>();
searchControl = new FormControl("");
query: TableQuery = { ...this.initialQuery };
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);
});
// Initial fetch
this.queryChange.emit(this.query);
}
get displayedColumns(): string[] {
return this.columns.map((c) => c.key as string);
}
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);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
3) ✅ Pro Table HTML (Material Professional)
<div class="table-wrapper mat-elevation-z2">
<div class="table-header">
<h2>{{ title }}</h2>
<mat-form-field appearance="outline" class="search-box">
<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>
</div>
<div class="loading-bar" *ngIf="loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<table mat-table [dataSource]="data" matSort (matSortChange)="onSortChange($event)" class="full-width">
<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">
{{ row[col.key as keyof T] }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
<tr class="mat-row" *ngIf="!loading && data.length === 0">
<td class="mat-cell empty" [attr.colspan]="displayedColumns.length">
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>
</div>
4) ✅ SCSS (Enterprise UI Look)
.table-wrapper {
padding: 16px;
border-radius: 12px;
background: #fff;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.search-box {
width: 320px;
}
.full-width {
width: 100%;
}
.empty {
text-align: center;
padding: 20px;
font-weight: 500;
opacity: 0.7;
}
.loading-bar {
margin: 10px 0;
}
5) ✅ Feature Example (Users Page)
export interface UserDto {
id: number;
name: string;
email: string;
role: string;
createdOn: string;
}
6) ✅ Users Service (Server-side filtering, sorting, paging)
import { HttpClient, HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { PagedResponse, TableQuery } from "../../shared/components/pro-table/pro-table.models";
import { UserDto } from "./users.models";
@Injectable({ providedIn: "root" })
export class UsersService {
private baseUrl = "https://localhost:7219/api/users";
constructor(private http: HttpClient) {}
getUsers(query: TableQuery): Observable<PagedResponse<UserDto>> {
let params = new HttpParams()
.set("pageIndex", query.pageIndex)
.set("pageSize", query.pageSize)
.set("search", query.search || "")
.set("sortBy", query.sortBy || "")
.set("sortDir", query.sortDir || "");
Object.keys(query.filters || {}).forEach((key) => {
if (query.filters[key] !== null && query.filters[key] !== undefined && query.filters[key] !== "") {
params = params.set(key, query.filters[key]);
}
});
return this.http.get<PagedResponse<UserDto>>(this.baseUrl, { params });
}
}
7) ✅ Users Component (Enterprise RxJS pattern)
import { Component } from "@angular/core";
import { BehaviorSubject, Observable, switchMap, catchError, of, tap } from "rxjs";
import { TableColumn, TableQuery, PagedResponse } from "../../shared/components/pro-table/pro-table.models";
import { UsersService } from "./users.service";
import { UserDto } from "./users.models";
@Component({
selector: "app-users",
templateUrl: "./users.component.html",
})
export class UsersComponent {
columns: TableColumn<UserDto>[] = [
{ key: "id", header: "ID", sortable: true },
{ key: "name", header: "Name", sortable: true },
{ key: "email", header: "Email", sortable: true },
{ key: "role", header: "Role", sortable: true },
{ key: "createdOn", header: "Created", sortable: true },
];
loading = false;
data: UserDto[] = [];
totalCount = 0;
private query$ = new BehaviorSubject<TableQuery>({
pageIndex: 0,
pageSize: 10,
search: "",
sortBy: "id",
sortDir: "asc",
filters: {},
});
usersRequest$: Observable<PagedResponse<UserDto>> = this.query$.pipe(
tap(() => (this.loading = true)),
switchMap((query) =>
this.usersService.getUsers(query).pipe(
catchError(() => of({ items: [], totalCount: 0 }))
)
),
tap((res) => {
this.data = res.items;
this.totalCount = res.totalCount;
this.loading = false;
})
);
constructor(private usersService: UsersService) {
this.usersRequest$.subscribe();
}
onQueryChange(query: TableQuery) {
this.query$.next(query);
}
}
8) Users Component HTML
<app-pro-table
title="Users (Enterprise Ultimate)"
[columns]="columns"
[data]="data"
[totalCount]="totalCount"
[loading]="loading"
(queryChange)="onQueryChange($event)"
></app-pro-table>
✅ Why this is "Enterprise Ultimate"
- Reusable component
- Generic type support <T>
- Server-side pagination
- Server-side sorting
- Search with debounce
- Cancel previous request (switchMap)
- Strong typing
- Loading state
- Clean architecture
Comments
Post a Comment