Angular Material Dynamic Table for any Data

Nowadays, the time required to deliver a market-ready product is becoming shorter and shorter. In just a few days, a highly efficient team or startup can produce a ready-to-test Minimum Viable Product (MVP). This rapid development is possible due to the availability of highly modular components provided openly by multiple suppliers worldwide. In the Angular developer community, one important factor contributing to this speed is the Angular Material component library.

Angular Material Table

Angular Material offers many useful components, but for displaying multiple items of data, the List and Table components are the most important. While the List component is versatile, the Table component is ideal for displaying raw data from individual tables in a relational database. It is also well-suited for multi-dimensional data visualization.

However, despite the Table component being ready to use with just a few simple setup steps, I found it almost impossible to use for my case, where I need to display data from more than one hundred PostgreSQL tables returned from a backend server.

Below is a basic setup of the Angular Material Table, version 17.x, showing how columns can be defined in both HTML and TypeScript files. Where the displayedColumns is determined in the .ts file, and again explicitly hard-coded in the .html file.

image

// component definition
export class TableBasicExample {
  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
  dataSource = ELEMENT_DATA;
}
// the rest of component

and HTML

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
  <!-- Position Column -->
  <ng-container matColumnDef="position">
    <th mat-header-cell *matHeaderCellDef> No. </th>
    <td mat-cell *matCellDef="let element"> {{element.position}} </td>
  </ng-container>

  <!-- Name Column -->
  <ng-container matColumnDef="name">
    <th mat-header-cell *matHeaderCellDef> Name </th>
    <td mat-cell *matCellDef="let element"> {{element.name}} </td>
  </ng-container>

  <!-- Weight Column -->
  <ng-container matColumnDef="weight">
    <th mat-header-cell *matHeaderCellDef> Weight </th>
    <td mat-cell *matCellDef="let element"> {{element.weight}} </td>
  </ng-container>

  <!-- Symbol Column -->
  <ng-container matColumnDef="symbol">
    <th mat-header-cell *matHeaderCellDef> Symbol </th>
    <td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
  </ng-container>

  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

Problem

Consider the use case where my backend is powered by TypeORM and includes entities reflecting the schema of a database with more than a hundred tables.

image

note

Assume that the backend provides APIs to:

  • GET a list of all possible Table APIs
  • GET/PUT/POST... API to manipulate a given table's data

In this scenario, it's not ideal to create a hundred variants of the table component to display various data sets with different column and row values. The requirement is to display original or transformed data with a predictable key-value structure, rather than irregular ones. For instance, the data fed into our table could be in JSON format, as shown below:

{
  "total": 10,
  "data": [
    {
      "position": 4,
      "name": "name-1",
      "age": 30,
      "active": true
    },
    {
      "position": 9,
      "name": "name-2",
      "age": 40,
      "active": false
    }
  ]
}

Will be presented in table like the below

#positionnameageactive
14name-130true
29name-240false

All-in-one Dynamic Table

The idea is to create a dynamic table based on the Angular Material Table to display whatever feeding data. The downside of this approach is it cannot handle specific requirements, i.e., to arbitrary color or transform name of a few tables. The pros is we maintain one but use in many places.

Below is the full source code of the Dynamic Table, that you may want to tweak it for your use case, i.e., only visualize data but not handle logic of fetching data inside the component.

import {AfterViewInit, Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from '@angular/core';
import {CommonModule} from "@angular/common";

import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
import {MatSort, MatSortModule} from '@angular/material/sort';
import {MatTableDataSource, MatTableModule} from '@angular/material/table';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';

import {DatabaseService} from "../../../services/database/database.service";

@Component({
  selector: 'app-dynamic-table',
  standalone: true,
  imports: [MatFormFieldModule, MatInputModule, MatTableModule, MatSortModule, MatPaginatorModule, CommonModule],
  templateUrl: './dynamic-table.component.html',
  styleUrl: './dynamic-table.component.scss'
})
export class DynamicTableComponent  implements AfterViewInit, OnChanges {
  @Input() dbTableName: string = '';

  pageSizes = [10, 30, 100];

  columns: any[] = [];
  displayedColumns: any[] = [];
  dataSource: MatTableDataSource<any> = new MatTableDataSource<any>([]);

  resultsLength = 0;

  @ViewChild(MatPaginator) paginator: MatPaginator | null = null;
  @ViewChild(MatSort) sort: MatSort | null = null;

  constructor(private service: DatabaseService) {
  }

  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
  }

  ngOnChanges(changes: SimpleChanges) {
    const { dbTableName } = changes;
    if (dbTableName.previousValue !== dbTableName.currentValue) {
      this.getData(dbTableName.currentValue);
    }

  }

  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();

    if (this.dataSource.paginator) {
      this.dataSource.paginator.firstPage();
    }
  }

  getData(table: string, page= 1, limit = this.pageSizes[0]): void {
    this.service.getData(table, page, limit)
      .subscribe(({ data, total }) => {
        this.resultsLength = total;

        // construct displayed columns
        if (data.length > 0) {
          this.columns = Object.keys(data[0]);
          this.displayedColumns = ['index', ...this.columns];
        }

        const convertedData = data.map((item: any) => {
          this.columns.forEach((column, index) => {
            // if the item contains an object, i.e., a JSON object, an array, etc., convert it to a string to be displayed on the table
            if (item[column] && typeof item[column] === 'object') {
              item[column] = JSON.stringify(item[column]);
            }
          });
          return item;
        });
        this.dataSource = new MatTableDataSource(convertedData);
      });
  }

  handlePageEvent(event: any) {
    this.getData(this.dbTableName, event.pageIndex + 1, event.pageSize);
  }
}

And here is its HTML

note

CSS class used in this HTML is powered by TailwindCSS

<div class="mat-elevation-z8 overflow-auto">
  <table mat-table [dataSource]="dataSource" matSort>
    <!-- To display the number (index) of rows -->
    <ng-container matColumnDef="index">
      <th mat-header-cell *matHeaderCellDef> # </th>
      <td mat-cell *matCellDef="let row; let i = index;" class="text-gray-300 italic border-r">{{i + 1}}</td>
    </ng-container>

    <ng-container *ngFor="let column of columns; let i = index" [matColumnDef]="column">
      <th mat-header-cell *matHeaderCellDef class="!font-semibold">{{ column }}<span class="text-gray-400 font-light text-xs pl-1">{{i + 1}}</span></th>
      <td mat-cell *matCellDef="let row">{{ row[column] }}</td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

    <!-- Row shown when there is no matching data. -->
    <tr class="mat-row" *matNoDataRow>
      <td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
    </tr>

  </table>

  <mat-paginator
    [length]="resultsLength"
    [pageSizeOptions]="pageSizes"
    (page)="handlePageEvent($event)"
    aria-label="Select page of users"
  />
</div>
:host {
  display: block;
  width: 100%;

  table {
    width: 100%;
  }

  .mat-mdc-form-field {
    width: 100%;
  }

  .mat-mdc-header-row,
  .mat-mdc-cell {
    font-size: 1rem;
  }

  .mat-mdc-row:hover {
    .mat-mdc-cell {
      background-color: rgba(0, 0, 0, 0.04);
    }
  }
}

Conclusion

  • The Dynamic Table above is implemented in Angular 17.x and Angular Material 17.x, but I expect it to be reusable in Angular 18.x too.
  • The problem we resolved with Dynamic Table is to develop and maintain in one place (component) but re-used in any possible key-value (schema/table) use case.
  • If you are looking for a customized table solution for specific data, then Dynamic (unified) Table is not for your use case.
  • Some tweaks you may want to consider is to separate the business logic from UI in the sample implementation above, where it handle the Data fetch operation in its code (init & paginating)

Related Posts

From MacOS to MS SQL Server with Windows Authentication

In a recent scenario, I found myself assisting a friend who had encountered a connectivity challenge with an old version of MS SQL database. The unique aspect of this situation was the authenticati

Read More

Angular Material Dynamic Table for any Data

Nowadays, the time required to deliver a market-ready product is becoming shorter and shorter. In just a few days, a highly efficient team or startup can produce a ready-to-test Minimum Viable Produc

Read More