Search code examples
angularangular-material-tableng-contentng-container

Angular MatTable with column definition from parent


Objectif: Creating a component app-table which would contain some definitions of columns that are common to multiple tables + giving inside the bracks some more definitions from the parent to the child

parent component

<app-table [data]="users" [displayedColumns]="displayedColumns" [busy]="busyLoading">
    <!-- action -->
<ng-container matColumnDef="actions">
        <th mat-header-cell *matHeaderCellDef>{{'COMMON.ACTION'}}</th>
        <td mat-cell *matCellDef="let row">
        <button type="button" [routerLink]="['/users/' + row.id]">edit</button>
        </td>
    </ng-container>

</app-table>

app-table component

<table mat-table [dataSource]="dataSource">
    <ng-container matColumnDef="firstName">
        <th scope="col" mat-header-cell *matHeaderCellDef>{{'USER.FIRST_NAME' | translate}}</th>
        <td mat-cell *matCellDef="let row">{{row.firstName}}</td>
    </ng-container>

<ng-container matColumnDef="lastName">
        <th scope="col" mat-header-cell *matHeaderCellDef>{{'USER.LASTE_NAME' | translate}}</th>
        <td mat-cell *matCellDef="let row">{{row.lastName}}</td>
    </ng-container>

    <!-- what I'm trying to do -->
<ng-content></ng-content>

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

</table>

the error I get:

ERROR Error: Could not find column with id "actions"


ADD:

This is a new approche

parent HTML

    <!-- parent.component.html -->
<h1>Parent Component</h1>
<app-user-list [dataSource]="users" [displayedColumns]="displayedColumns">
    <!-- Custom columns for UserListComponent -->
    <ng-container matColumnDef="lastname">
        <th mat-header-cell *matHeaderCellDef>Last Name</th>
        <td mat-cell *matCellDef="let user">{{ user.lastname }}</td>
    </ng-container>
</app-user-list>

typescript

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

interface User {
    id: number;
    firstname: string;
    lastname: string;
}

@Component({
    selector: 'app-parent',
    templateUrl: './parent.component.html',
    styleUrls: ['./parent.component.scss']
})
export class ParentComponent {

    users: User[] = [
        { id: 1, firstname: 'John', lastname: 'Doe' },
        { id: 2, firstname: 'Jane', lastname: 'Smith' },
        { id: 3, firstname: 'Alice', lastname: 'Johnson' },
        { id: 4, firstname: 'Bob', lastname: 'Brown' },
    ];
    displayedColumns: string[] = [
        'id',
        'firstname',
        'lastname'
    ];

    constructor() { }
}

child html

<!-- user-list.component.html -->
<mat-card>
    <mat-card-header>
        <mat-card-title>User List</mat-card-title>
    </mat-card-header>
    <mat-card-content>
        <table mat-table [dataSource]="dataSource">
            <!-- Default columns (ID and First Name) -->
            <ng-container matColumnDef="id">
                <th mat-header-cell *matHeaderCellDef>ID</th>
                <td mat-cell *matCellDef="let user">{{ user.id }}</td>
            </ng-container>
            <ng-container matColumnDef="firstname">
                <th mat-header-cell *matHeaderCellDef>First Name</th>
                <td mat-cell *matCellDef="let user">{{ user.firstname }}</td>
            </ng-container>

            <!-- Custom columns (added using ng-content) -->
            <ng-content select="[matColumnDef='lastname']"></ng-content>

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

typescript

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


@Component({
    selector: 'app-user-list',
    templateUrl: './user-list.component.html',
    styleUrls: ['./user-list.component.scss']
})
export class UserListComponent {
    @Input() dataSource: any;
    @Input() displayedColumns: string[] = [];

    constructor() { }
}

How can I display the column lastname without having to define it inside the userList


Solution

  • You can use the function: addColumnDef of a MatTable

    So the only is defined your ColumnDef outside the app-user-list and pass as input

    You get it using ViewChildren

      @ViewChildren(MatColumnDef) columns:QueryList<MatColumnDef>
    

    And define like:

    <table-basic-example
      [data]="dataSource"
      [columnAdd]="columns"
      [displayedColumns]="displayedColumns"
    >
    </table-basic-example>
    
    <ng-container matColumnDef="name">
      <th mat-header-cell *matHeaderCellDef>First Name</th>
      <td mat-cell *matCellDef="let user">{{ user.name }}</td>
    </ng-container>
    
    <ng-container matColumnDef="actions">
      <th mat-header-cell *matHeaderCellDef>Actions</th>
      <td mat-cell *matCellDef="let row">
        <button type="button" (click)="edit(row)">edit</button>
      </td>
    </ng-container>
    

    The only is, in afterViewInit in your componnet add this columnsDef and add the columns

      @Input() columnAdd: QueryList<MatColumnDef>;
      @ViewChild(MatSort) sort: MatSort;
      @ViewChild(MatTable) table: MatTable<any>;
    
      ngAfterViewInit() {
        this.dataSource.sort = this.sort;
        setTimeout(() => {
          this.columnAdd.forEach((x) => {
            this.table.addColumnDef(x);
          });
    
          this.displayedColumns2.push('name');
          this.displayedColumns2.push('actions');
        });
      }
    

    a stackblitz

    NOTE: Really you can add the columns definitions inside or outside the tag of the component

    Update if we put the matColumnDef outside the component we can use a *ngIf to not show the component until Angular get it

    <table-basic-example *ngIf="columns && columns.length"..>
    </table-basic-example>
    
    <ng-container matColumnDef="name">
      ..
    </ng-container>
    
    <ng-container matColumnDef="actions">
      ..
    </ng-container>
    
    To check:
    {{columns?.length || 'I am not get it'}}
    

    Avoid the ngAfterChecked error required a work-aroud. The problem is that "columns" are ready only in ngAfterViewInit, so Angular give this error always. We can use a variable

    yet:boolean=false
    

    And in parent in ngOnInit use a setTimeout()

    ngOnInit(){
      setTimeout(()=>{
         this.yet=true;
      })
    }
    

    And use a *ngIf="yet"