24

Instead of a regular way of displaying data to table. I'm trying to create my custom-table component and project the data in the material table via .

like this:

<table mat-table [dataSource]="dataSource">
 <!-- I want to Render the header and cell data here -->
 <ng-content></ng-content>
 
 <mat-header-row *matHeaderRowDef="headers; sticky: true"></mat-header-row>
 <mat-row *matRowDef="let row; columns: headers;"></mat-row>
</table>

So I can call this customized component like this:

<app-customized-table>
 <ng-container matColumnDef="id">
  <mat-header-cell *matHeaderCellDef> Id </mat-header-cell>
  <mat-cell *matCellDef="let element"> {{element.Id}} </mat-cell>
 </ng-container>
 ...etc
</app-customized-table>

However, it won't detect the content. Here's a stackblitz example that i'm trying to do.

https://stackblitz.com/edit/angular-qwvcln?file=src%2Fapp%2Fcustom-table.component.ts

Is there a way to do this?

Thanks.

jedion
  • 624
  • 1
  • 7
  • 18

3 Answers3

45

Bit late to the party, but I had the same challenge and was able to solve it as follows:

You can solve it by adding the column definitions to the table programmatically, by using the @ContentChildren and @ViewChild functionality:

your template file (e.g. customized-table.component.html):

<table mat-table [dataSource]="dataSource">
    <ng-content></ng-content>

    <mat-header-row *matHeaderRowDef="headers; sticky: true"></mat-header-row>
    <mat-row *matRowDef="let row; columns: headers;"></mat-row>
</table>

your codebehind (e.g. customized-table.component.ts):

@Component({
    selector: 'app-customized-table',
    templateUrl: './customized-table.component.html'
})
export class CustomizedTableComponent<T> implements AfterContentInit {
    constructor() { }

    @Input() dataSource: T[]; // or whatever type of datasource you have
    @Input() headers: string[];

    // this is where the magic happens: 
    @ViewChild(MatTable, { static: true }) table: MatTable<T>;
    @ContentChildren(MatColumnDef) columnDefs: QueryList<MatColumnDef>;

    // after the <ng-content> has been initialized, the column definitions are available.
    // All that's left is to add them to the table ourselves:
    ngAfterContentInit() {
        this.columnDefs.forEach(columnDef => this.table.addColumnDef(columnDef));
    }
}

Now, you're able to use:

<app-customized-table [headers]="headers">
    <ng-container matColumnDef="id">
        <mat-header-cell *matHeaderCellDef> Id </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.Id}} </mat-cell>
    </ng-container>
    ...etc
</app-customized-table>

Stackblitz working demo with lazy loaded modules

yaba
  • 829
  • 7
  • 11
  • I have tried this one but it doesn't work, How you managed to make it work? My situation is that the table load inside a lazyloading module, and the error gave me it is cannot find id column..really need a work around of this otherwise The code has to be repeatedly written which is not maintainable since my application needs tons of tables. – Johnathan Li Oct 15 '19 at 08:58
  • Did you put the `CustomizedTableComponent` in a [shared module](https://angular.io/guide/sharing-ngmodules) and import that module in your lazy loaded module? – yaba Oct 16 '19 at 08:10
  • I'm running into the same error as @JohnathanLi. Could not find column with id [the first column def id]. Can you post the code on Stackblitz? – Stefan Norberg Nov 15 '19 at 13:42
  • Never mind, got it to work now. One improvement would be to remove the [headers] bindning and use this.headers = this.columnDefs.map(d=> d.name); – Stefan Norberg Nov 15 '19 at 14:16
  • 1
    @Stefan Added a stackblitz example.. You solution to use the `columnDefs` for the `displayedColumns` takes away the option to choose which columns to display and in what order independent of the column definitions. Make sure the MatTableModule is imported somehow into the module where you're using the customized table component (see stackblitz example) – yaba Nov 15 '19 at 15:22
  • Many thanks. I don't think you can get sorting to work with this approach though? – Stefan Norberg Nov 15 '19 at 15:48
  • 1
    Sure you can! Just add `@ViewChild(MatSort, { static: true }) sort: MatSort;` in your component in which you add your table (so *not* the customized table component), and make sure you have `mat-sort-header` attribute on you header cells on which you would want to be able to sort. Then use `this.sort` to implement your sorting logic. – yaba Nov 15 '19 at 16:08
  • From your stackBlitz, sorting is not applied on columns. This problem only occurs for content projection. Any idea? mat-sort-header is also present on your columns – Zonaib Apr 30 '20 at 10:37
  • I'm not entirely sure what you're asking, but sorting works with this solution too. For more info, see https://material.angular.io/components/sort/overview – yaba Apr 30 '20 at 13:19
  • @yaba, there are no sort icons appearing in header of the columns, even though, your column templates have mat-sort. – Zonaib May 02 '20 at 19:09
  • Thank you! It's a really good approach to solve this pretty common pattern! +1 – Emilio Numazaki May 05 '20 at 04:00
  • @Zonaib I updated the stackblitz project so that the sorting arrows are displayed. As you can see, you can use any angular table feature you want. (ps. the actual sorting hasn't been implemented, because I want to keep the example as simple as possible to demonstrate how you can use content projection with a custom table) – yaba May 06 '20 at 08:07
  • @yaba, i can see that the only change you did was adding matsort on your customized-table, and no matsort on your actual table. Does not work for me if i add the same. If it's a version issue, then I am using my anguar 7.2, material 7.3. Did i miss anything? Any advice? – Zonaib May 06 '20 at 08:55
  • @Zonaib you also have to import the MatSortModule (and the BrowserAnimationModule or NoopAnimationModule). Then to add sorting functionality add a \@ViewChild to get a reference to your matSort (where you use the custom table) and use that to add sorting functionality. For more info, see material.angular.io/components/sort/overview – yaba May 06 '20 at 09:06
  • Thank you @yaba, that works, I had forgotten to import MatSort in my component where I added my table. Upon further exploration, I get the matsort event in this component, not the customized table component. Is there a way I could delegate this to customized table? The reason is I want to delegate most common tasks to this customized table. – Zonaib May 06 '20 at 10:14
  • @Zonaib I think you can add the matSort directive to any container of the column definitions, even when content projected. But you'd have to try it out to see if it works. – yaba May 06 '20 at 11:11
  • For some reason I get undefined table. @ViewChild(MatTable...) doesn't get the table! Why could that be? – letie May 18 '20 at 18:59
  • 1
    @LetieTechera do you have your table in a conditional container (e.g. using `*ngIf`)? If so, then use `static: false`. Better yet, download the stackblitz project and work your way from there. – yaba May 19 '20 at 11:01
  • @yaba 'do you have your table in a conditional container' => that was the problem. it worked. thanks!! – letie May 19 '20 at 21:25
  • @yaba What is not working is the sorting.. I have matSort in "app-customized-table" tag, because otherwise the html brokes, and then mat-sort-header in each sortable column... The header appears to be "sortable" but it does not do anything – letie May 20 '20 at 17:47
  • 1
    @yaba I have MatSort Module on my module imports, I added @ViewChild(MatSort, { static: true }) sort: MatSort; and this.sort. it does not work. I can't see it working in your example either – letie May 20 '20 at 17:51
  • @StefanNorberg. Can you remember what you did to remove the error: 'Could not find column with id xxx'. I have the same issue and just can't get rid of it. – Sun Apr 06 '21 at 13:45
  • I solved the sorting problem! My solution is here: https://stackblitz.com/edit/angular-customized-table-jde2uj?file=src/app/shared/customized-table/customized-table.component.ts – Rajdeep Chatterjee Mar 04 '22 at 07:37
2

You would have to take a very different approach to get this to work. As you have noticed, mat-table can't have it's content wrapped in something else. A possibility is to provide just the data to your custom component rather than the the DOM as content. For example:

Component:

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-custom-table',
  template: `
    <div>
      {{name}}

      <div class="example-container mat-elevation-z8">
        <table mat-table [dataSource]="dataSource">

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

          <ng-container *ngFor="let column of displayedColumns; index as i" >

            <ng-container [matColumnDef]="column">
              <mat-header-cell *matHeaderCellDef>{{ columnLabels[i] }}</mat-header-cell>
              <mat-cell mat-cell *matCellDef="let element"> {{element[valueKeys[i]]}} </mat-cell>
            </ng-container>

          </ng-container>

        </table>
      </div>

      asd
    </div>
  `
})
export class CustomTable implements OnInit {
  @Input() public columnLabels: any[];
  @Input() public displayedColumns;
  @Input() public dataSource;
  @Input() public name: any;
  @Input() public valueKeys: any[];

  constructor() { }

  ngOnInit() {
  }

}

Usage:

<app-custom-table name="sdf" 
    [dataSource]="dataSource" 
    [displayedColumns]="displayedColumns"
    [columnLabels]="columnLabels" 
    [valueKeys]="displayedColumns">

</app-custom-table>

Where:

displayedColumns = ['position', 'name', 'weight', 'symbol'];
dataSource = ELEMENT_DATA;
columnLabels = ['No.', 'Name', 'Weight', 'Symbol'];

But even this way there might be some more challenges.

G. Tranter
  • 16,766
  • 1
  • 48
  • 68
  • Thanks. This is a nice one., but you're right challenges will come along the way, and one is that it's hard to customize data per cell. Well, that actually is my aim in the first place. – jedion Dec 04 '18 at 15:01
  • Ended up going with this. The only solution that works when you want to wrap your table and have another component between it, and the component that passes data to it. The accepted ng-content solution breaks down at that point because ContentChildren can no longer access projected ng-content. – Eternal21 Aug 30 '21 at 00:27
0

You can project content like this:

<ng-container *ngFor=”let column of displayedColumns” matColumnDef=”{{column.id}}”>
   <th mat-header-cell *matHeaderCellDef mat-sort-header>{{column.name}}</th>
   <td mat-cell *matCellDef=”let row”>
      {{row[column.id]}}
   </td>
</ng-container>

Then you pass the template for content projection:

<my-component …
[itemTemplate]=”itemTemplate”>
   <ng-template let-item #itemTemplate>
      column id: {{item.column.id}}
      value: {{item.value}}
   </ng-template>
</my-component>

source

Tom Smykowski
  • 25,487
  • 54
  • 159
  • 236
  • I didn't find the snippets you shared here very useful as they are missing the whole `ngTemplateOutlet` concept, but your article makes a good explanation of it and turned out to be exactly what I needed. – Aebsubis Jan 18 '23 at 15:15