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

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