2
votes

I have an angular material table which I need to generate some somewhat repetitive columns for. So I made a component that generates the repeating columns and attempted to inject it into my mat-table, but it does not appear to work. Am I missing something obvious? I've built tables with dynamically named columns before but I did it within the same component as the table itself, so I'm thinking it's an issue of being contained within another element or something.

I believe this is possible, just can't find the right terms to google. Tried to make the example as simple as I could. The 'property' attribute on the repetitive-columns component helps to dynamically generate unique column names to feed back to the displayedColumns property for the mat-table

The displayedColumns value gets updated naively in this example and generates a ExpressionChangedAfterItHasBeenCheckedError, but appears to me like it should work otherwise:

enter image description here

https://stackblitz.com/edit/angular-ivy-oe79ho?file=src%2Fapp%2Fmy-dynamic-table%2Fmy-dynamic-table.component.html

Table Template

<table mat-table [dataSource]="ds">
    <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef>SKU</th>
        <td mat-cell *matCellDef="let c" class="text-uppercase">
            {{ c.sku }}
        </td>
        <td mat-footer-cell *matFooterCellDef></td>
    </ng-container>

    <!-- Comment the following 2 components to see how the 'displayedColumns' changes, and the table works with both commented -->
    <app-my-repetitive-columns title="Base" property="base"></app-my-repetitive-columns>
    <app-my-repetitive-columns title="Committed" property="committed"></app-my-repetitive-columns>

    <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" sticky></tr>
    <tr mat-row *matRowDef="let c; let i = dataIndex; columns: displayedColumns; let even = even"
        [class.exclusions]="c.hasExclusions" [class.striped]="even"></tr>
</table>

"Repetitive Columns" Template

<!-- With a supplied input value of "base" for "property", the column name here should be 'base-sales' -->
<ng-container [matColumnDef]="columnNames[0]">
    <th mat-header-cell *matHeaderCellDef>{{title}} Sales</th>
    <td mat-cell *matCellDef="let c" class="text-uppercase">
      {{ c[property].sale }}
    </td>
    <td mat-footer-cell *matFooterCellDef></td>
  </ng-container>

  <ng-container [matColumnDef]="columnNames[1]">
    <th mat-header-cell *matHeaderCellDef>{{title}} Impact</th>
    <td mat-cell *matCellDef="let c" class="text-uppercase">
      {{ c[property].impact }}
    </td>
    <td mat-footer-cell *matFooterCellDef></td>
  </ng-container>

  <ng-container [matColumnDef]="columnNames[2]">
    <th mat-header-cell *matHeaderCellDef>{{title}} Impact %</th>
    <td mat-cell *matCellDef="let c" class="text-uppercase">
      {{ c[property].impactPct }}
    </td>
    <td mat-footer-cell *matFooterCellDef></td>
  </ng-container>
1

1 Answers

1
votes

OK, I figured this out finally.

Found this question: Angular Material mat-table define reusable column in component

which led me here: https://github.com/angular/components/issues/13808

In short, you have to manually tell the table about the column definitions. This is accomplished by injecting a reference to the MatTable into your child component.

So the constructor for the 'my-repetitive-columns' looks like:

constructor(private _table: MatTable<any>) {
  // this._table is a reference to the mat-table element this component is a child of
}

Then, define a ViewChildren querylist for the new MatColumnDefs in the component:

@ViewChildren(MatColumnDef) columnDefs: QueryList<MatColumnDef>;

Then in ngAfterViewInit():

ngAfterViewInit() {
  this.updateColumnDefs(); // first pass

  // updates ColumnDefs on any changes
  this.columnDefs.changes.subscribe(() => {
    this.updateColumnDefs();
  }); //make sure you unsubscribe!
}

updateColumnDefs() {
  // this attaches the column definition to the table (does nothing about disappearing columns, etc)
  this.columnDefs.forEach((def) => this._table.addColumnDef(def));
}

I additionally have other function to define the column names, and feed back WHICH of those should be displayed to the table itself (while still letting the parent table component decide which order everything should be in), but that's specific to my implementation so will leave it out for the purposes of this answer.