angulartypescriptangular-materialangular-datatables

How to reorder columns in data table?


I want to have the columns to be able to be reordered or moved around for a data table. The code implemented isn't working. Any time I try to reorder a column the column either gets stuck when trying to reorder or just doesn't work at all. I need assistance on what I am missing from the code. This code is what I've added to my existing code.

html

  <th mat-header-cell *matHeaderCellDef
  cdkDropList
  cdkDropListLockAxis="x"
  cdkDropListOrientation="horizontal"
  (cdkDropListDropped)="dropListDropped($event, i)"
  cdkDrag
  (cdkDragStarted)="dragStarted($event, i)"
  [cdkDragData]="{name: column.field, columIndex: i}" >
           ..................
          ..................
   </th>

ts

columns: any[] = [
  { field: 'position' },
  { field: 'name' },
  { field: 'weight' },
  { field: 'symbol' }
];

previousIndex: number;

setDisplayedColumns() {
  this.columns.forEach(( colunm, index) => {
    colunm.index = index;
    this.displayedColumns[index] = colunm.field;
  });
}

dragStarted(event: CdkDragStart, index: number ) {
  this.previousIndex = index;
}

dropListDropped(event: CdkDropList, index: number) {
  if (event) {
    moveItemInArray(this.columns, this.previousIndex, index);
    this.setDisplayedColumns();
  }
}

Demo - Stackblitz


Solution

  • Issues

    1. You are missing the cdkDropListGroup attribute in the root (<mat-table>) which it is required to get the index (position) of dropped element.

    2. You have to use trackBy in *ngFor to track which column (name) is changed to do the column reordering. Without it, the performance for rendering degrade when each time changes and may face the page crash.

    3. When the columns are reordered, you are not correctly assign the displayedColumns and based on your existing logic, you may lose some columns such as: select, toggle.

    4. It seems sometimes the ordering is not working correcly due to the previous index. Refer to this demo, you should trace the current index of dragged item.

    Assume that you want to filter those value columns (columns), I would suggest to split out the "action" column from the columnsToDisplay and make it a standalone column with <ng-container> similar to the "select" column.

    <table
      mat-table
      [dataSource]="dataSource"
      multiTemplateDataRows
      class="mat-elevation-z8"
      cdkDropListGroup
    >
      <!-- toggle columns menu -->
      <ng-container matColumnDef="{{toggleColDef}}">
        <th mat-header-cell *matHeaderCellDef>
          <button mat-button [matMenuTriggerFor]="menu" class="table-config-menu">
            <mat-icon aria-hidden="false" aria-label="Example home icon"
              >settings</mat-icon
            >
            <mat-menu #menu="matMenu">
              <span class="table-config-menu-label">Edit Columns</span>
              <div
                class="table-config-menu-options"
                style="display: flex; flex-direction: column; padding: 8px"
              >
                <mat-checkbox
                  *ngFor="let cd of columnDefinitions; let i = index"
                  [checked]="isColumnVisible(cd.def)"
                  (change)="toggleColumn(cd.def, $event.checked)"
                >
                  {{cd.label}}
                </mat-checkbox>
              </div>
            </mat-menu>
          </button>
        </th>
        <td mat-cell *matCellDef="let element"></td>
      </ng-container>
    
      <ng-container
        matColumnDef="{{column}}"
        *ngFor="let column of columnsToDisplay; let i = index; trackBy column"
      >
        <ng-container>
          <th
            mat-header-cell
            *matHeaderCellDef
            cdkDropList
            cdkDropListLockAxis="x"
            cdkDropListOrientation="horizontal"
            (cdkDropListDropped)="dropListDropped($event, column)"
            cdkDrag
            (cdkDragStarted)="dragStarted($event, i)"
            [cdkDragData]="{name: column, columIndex: i}"
          >
            {{column}}
          </th>
          <td mat-cell *matCellDef="let element">{{element[column]}}</td>
        </ng-container>
      </ng-container>
      <ng-container matColumnDef="action">
        <th mat-header-cell *matHeaderCellDef>Actions</th>
        <td mat-cell *matCellDef="let element" class="checkbox-spacing">
          <mat-icon (click)="toggleExpandElement(element)"
            >{{isElementExpanded(element) ? 'expand_less' :
            'expand_more'}}</mat-icon
          >
        </td>
      </ng-container>
      <ng-container matColumnDef="select">
        <th mat-header-cell *matHeaderCellDef>
          <mat-checkbox
            (change)="$event ? toggleAllRows() : null"
            [checked]="selection.hasValue() && isAllSelected()"
            [indeterminate]="selection.hasValue() && !isAllSelected()"
            [aria-label]="checkboxLabel()"
          >
          </mat-checkbox>
        </th>
        <td mat-cell *matCellDef="let row">
          <mat-checkbox
            (click)="$event.stopPropagation()"
            (change)="$event ? selection.toggle(row) : null"
            [checked]="selection.isSelected(row)"
            [aria-label]="checkboxLabel(row)"
          >
          </mat-checkbox>
        </td>
      </ng-container>
    
      <!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
    
      <ng-container matColumnDef="expandedDetail">
        <td
          mat-cell
          *matCellDef="let element"
          [attr.colspan]="columnsToDisplayWithExpand.length"
        >
          <div
            class="example-element-detail"
            [@detailExpand]="isElementExpanded(element) ? 'expanded' : 'collapsed'"
          >
            <div class="example-element-diagram">
              <div class="example-element-position">{{element.position}}</div>
              <div class="example-element-symbol">{{element.symbol}}</div>
              <div class="example-element-name">{{element.name}}</div>
              <div class="example-element-weight">{{element.weight}}</div>
            </div>
            <div class="example-element-description">
              {{element.description}}
              <span class="example-element-description-attribution">
                -- Wikipedia
              </span>
            </div>
            <div class="example-element-name">{{element.Name}}</div>
            <div class="example-element-weight">{{element.weight}}</div>
          </div>
        </td>
      </ng-container>
    
      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr
        mat-row
        *matRowDef="let element; columns: displayedColumns;"
        class="example-element-row"
        [class.example-expanded-row]="isElementExpanded(element)"
      ></tr>
      <tr
        mat-row
        *matRowDef="let row; columns: ['expandedDetail']"
        class="example-detail-row"
      ></tr>
      <!-- <tr mat-header-row *matHeaderRowDef="getDisplayedColumns()">
    </tr>
    <tr mat-row *matRowDef="let row; columns: getDisplayedColumns()"></tr>  -->
    </table>
    
    columnsToDisplay: string[] = [
      'Name',
      'weight',
      'symbol',
      'position',
    ];
    
    columns: any[] = ['Name', 'weight', 'symbol', 'position'];
    
    columnsToDisplayWithExpand = ['select', 'action', ...this.columnsToDisplay];
    
    dragStarted(event: CdkDragStart, index: number) {
      const prevIndex = this.columns.indexOf(event.source.data.name);
      this.previousIndex = prevIndex;
    }
    
    dropListDropped(
      event: CdkDragDrop<any>,
      targetColumn: string
    ) {
      if (event) {
        const dropIndex = this.columns.indexOf(targetColumn);
    
        console.log(
          `${event.item.data.name} Move from ${this.previousIndex} to ${dropIndex}`
        );
    
        moveItemInArray(this.columns, this.previousIndex, dropIndex);
    
        this.setDisplayedColumns();
      }
    }
    
    setDisplayedColumns() {
      this.displayedColumns = [
        'select',
        'action',
        ...this.columns,
        this.toggleColDef,
      ];
    }
    

    Demo @ StackBlitz