Search code examples
angularangular-materialangular-material-table

placing several common columns for angular table in an ng-template


I'm using angular 15.1 with angular material, I use angular material Table component to show tables.

my project have like 20 different tables that share 90% percent of the columns definitions so i would to place the shared columns in a ng-template instead of redefining the shared columns over and over again.

so I created the following template:

<ng-template #stats_common_columns>

  <ng-container matColumnDef="col1">
    <th mat-header-cell *matHeaderCellDef  mat-sort-header> Header1 </th>
    <td mat-cell *matCellDef="let element"> {{element['col1']}} </td>
  </ng-container>
  <ng-container matColumnDef="col2">
    <th mat-header-cell *matHeaderCellDef  mat-sort-header> Header2 </th>
    <td mat-cell *matCellDef="let element"> {{element['col2']}} </td>
  </ng-container>

  ...
</ng-template>

and then in the table definition i do the following:

<table [ngStyle]="{'display': isLoading ? 'none' : 'block'}" mat-table
       matSort #sortedAgentTableTop="matSort" multiTemplateDataRows
       [dataSource]="dataSource"
       class="mat-elevation-z8">


  <ng-container *ngTemplateOutlet="stats_common_columns"></ng-container>

  ....
</table>

unfortunately for all the ng-template related columns i get the error Could not find column with id X so obviously this is not the way to go.

well i actually want the ng-template columns to be in a separate file to be loaded by the other table components but for now i'm trying on the same file just to understand how it works.

any ideas?


Solution

  • MatTable which is build on top of CdkTable is using internally @ContentChildren() decorator to get all cdkColumnDef(in this case matColumnDef) directives from content you place beetwen <table></table> tags.

    Unfortunately it works only for directives literally placed between parent component tags, and do not return items that are placed in DOM in runtime, like in your case templates rendered by ngTemplateOutlet

    There is open issue in angular repo, take a look at this answer explaining philosophy behind it: https://github.com/angular/angular/issues/14842#issuecomment-470167258

    Walkaround: You can dynamically add columns to matTable using addColumnDef() method. Prepare component which hold your reusable column definitions like this:

    reusable-columns.component.ts

    import { MatColumnDef, MatTable } from '@angular/material/table';
    
    @Component({
      selector: 'app-reusable-columns',
      template: `
        <ng-container matColumnDef="symbol">
          <th mat-header-cell *matHeaderCellDef> Symbol </th>
          <td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
        </ng-container>
        <ng-container matColumnDef="symbol2">
          <th mat-header-cell *matHeaderCellDef> Symbol 2</th>
          <td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
        </ng-container>
      `,
    })
    export class ReusableColumnsComponent {
      @ViewChildren(MatColumnDef) cols: QueryList<MatColumnDef>;
    
      constructor(
        private table: MatTable<any>,
        private cdr: ChangeDetectorRef
      ) {}
    
      ngOnInit(): void {
        this.cdr.detectChanges();
        this.cols.forEach((col: MatColumnDef) => {
          this.table.addColumnDef(col)
        })
      }
    }
    

    Your consumer component using MatTable:

    <table mat-table>
      
      <!-- here are columns specific for only this table, defined in standard way -->
      <!-- 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>
    
      <!-- here you place your reusable columns -->
      <app-reusable-columns></app-reusable-columns>
    
      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
    
    

    Here is stackblitz example build on top of official mat-table example: https://stackblitz.com/edit/angular-8dcuqe-bep1qh?file=src/app/table-basic-example.html