Search code examples
angulartypescriptangular-materialangular7angular8

Duplicate Column Name Provided: Angular Material Data Table


For some reason I'm getting a error when making a checkbox column for my data table.

I understand the error from the console, but what can I do to fix it?

view.component.html

<mat-card-content>
<div class="view-container mat-elevation-z8">
  <table mat-table [dataSource]="dataSource" matSort>

    <ng-container matColumnDef="column" *ngFor="let column of displayedColumns">
      <th mat-header-cell *matHeaderCellDef>
        <mat-checkbox (change)="$event ? masterToggle() : null"
                      [checked]="selection.hasValue() && isAllSelected()"
                      [indeterminate]="selection.hasValue() && !isAllSelected()">
        </mat-checkbox>
      </th>
      <td mat-cell *matCellDef="let action">
        <mat-checkbox (click)="$event.stopPropagation()"
                      (change)="$event ? selection.toggle(row) : null"
                      [checked]="selection.isSelected(row)">
        </mat-checkbox>
      </td>
    </ng-container>

    <ng-container [matColumnDef]="column" *ngFor="let column of displayedColumns">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>
        {{ column }}
        <mat-icon aria-hidden="false" aria-label="filter icon">more_horiz</mat-icon>
      </th>
      <td mat-cell *matCellDef="let action">{{ action[column] }}
      </td>
    </ng-container>

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

  <mat-paginator [pageSizeOptions]="pageSizeOptions" showFirstLastButtons></mat-paginator>
</div>

view.component.ts

displayedColumns: string[] = [];

const displayedColumns = this.viewData.Columns.map((c: { Name: any; }) => c.Name);
    displayedColumns[2] = 'Folder1';
    this.displayedColumns = displayedColumns;
    // tslint:disable-next-line: max-line-length
    const fetchedData = this.viewData.DataRows.map((r: { slice: (arg0: number, arg1: number) => { forEach: (arg0: (d: any, i: string | number) => any) => void; }; }) => {
      const row = {};
      r.slice(0, 9).forEach((d: any, i: string | number) => (row[this.displayedColumns[i]] = d));
      return row;
    });

Solution

  • I think your problem is you are using each value in displayedColumns more than one time for in the matColumnDef First here:

    ...
        <ng-container matColumnDef="column" *ngFor="let column of displayedColumns">
          <th mat-header-cell *matHeaderCellDef>
            <mat-checkbox (change)="$event ? masterToggle() : null"
                          [checked]="selection.hasValue() && isAllSelected()"
                          [indeterminate]="selection.hasValue() && !isAllSelected()">
            </mat-checkbox>
    ...
    

    Then here:

    ...
       <ng-container [matColumnDef]="column" *ngFor="let column of displayedColumns">
          <th mat-header-cell *matHeaderCellDef mat-sort-header>
            {{ column }}
            <mat-icon aria-hidden="false" aria-label="filter icon">more_horiz</mat-icon>
          </th>
          <td mat-cell *matCellDef="let action">{{ action[column] }}
          </td>
        </ng-container>
    ...
    

    In fact, if you need a checkbox per row you don't need to create a mat-checkbox per displayedColumns.

    Here we have the example of Angular Material: https://material.angular.io/components/table/overview#selection

    So, you should do the following changes:

    1) Add the select column to displayedColumns:

    view.component.ts

    displayedColumns: string[] = [];
    
    const displayedColumns = this.viewData.Columns.map((c: { Name: any; }) => c.Name);
        displayedColumns[2] = 'Folder1';
        this.displayedColumns = ['select'].concat(displayedColumns);
        // tslint:disable-next-line: max-line-length
        const fetchedData = this.viewData.DataRows.map((r: { slice: (arg0: number, arg1: number) => { forEach: (arg0: (d: any, i: string | number) => any) => void; }; }) => {
          const row = {};
          r.slice(0, 9).forEach((d: any, i: string | number) => (row[this.displayedColumns[i]] = d));
          return row;
        });
    

    2) The first column (checkbox) should be select and it shouldn't be included in the *ngFor of the other columns:

    view.component.html

    <mat-card-content>
    <div class="view-container mat-elevation-z8">
      <table mat-table [dataSource]="dataSource" matSort>
    
        <ng-container matColumnDef="select">
          <th mat-header-cell *matHeaderCellDef>
            <mat-checkbox (change)="$event ? masterToggle() : null"
                          [checked]="selection.hasValue() && isAllSelected()"
                          [indeterminate]="selection.hasValue() && !isAllSelected()">
            </mat-checkbox>
          </th>
          <td mat-cell *matCellDef="let row">
            <mat-checkbox (click)="$event.stopPropagation()"
                          (change)="$event ? selection.toggle(row) : null"
                          [checked]="selection.isSelected(row)">
            </mat-checkbox>
          </td>
        </ng-container>
    
        <ng-container [matColumnDef]="column" *ngFor="let column of displayedColumns | slice:1 ">
          <th mat-header-cell *matHeaderCellDef mat-sort-header>
            {{ column }}
            <mat-icon aria-hidden="false" aria-label="filter icon">more_horiz</mat-icon>
          </th>
          <td mat-cell *matCellDef="let action">{{ action[column] }}
          </td>
        </ng-container>
    
        <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
      </table>
    
      <mat-paginator [pageSizeOptions]="pageSizeOptions" showFirstLastButtons></mat-paginator>
    </div>
    

    Now it should work as expected.