Angular MatTable with column definition from parent

360 Views Asked by At

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

1

There are 1 best solutions below

4
Eliseo On BEST ANSWER

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"