Reorder mat-table rows with angular material's drag-and-drop
Asked Answered
G

7

32

Angular 7 brought the powerful DragDropModule with it: https://material.angular.io/cdk/drag-drop/examples

The documentation deals with rearranging items within lists or transferring items between several lists. However, it doesn't talk about tables.

I was wondering whether there is a comfortable way of using angular material's drag-and-drop system for reordering rows in mat-table or cdk-table.

(You can add cdkDropList to mat-table which makes the mechanism work but without all the fancy animations and default drag placeholders.)

Does something like an easy-to-implement default for sorting table rows via drag-and-drop exist?

Gravestone answered 19/11, 2018 at 15:6 Comment(1)
You described that the mechanism is working. For me it is not working using the actual angular 7.1 . When I am adding a cdkDropList to the mat-table, I always get an Uncaught TypeError: Cannot read property 'clientRect' of undefined.Toffey
S
35

The styling is done by CSS (look at the CSS tab on the example page). I tweaked it to work with mat-table:

.cdk-drag-preview {
  box-sizing: border-box;
  border-radius: 4px;
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
              0 8px 10px 1px rgba(0, 0, 0, 0.14),
              0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.cdk-drag-placeholder {
  opacity: 0;
}

.cdk-drag-animating {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

.cdk-drop-list-dragging .mat-row:not(.cdk-drag-placeholder) {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

I placed this in my main styles.scss file.


For anyone wondering how to implement drag and drop on a mat-table, you need to:

  1. Add cdkDropList to mat-table
  2. Add (cdkDropListDropped)="onListDrop($event)" to mat-table
  3. Add cdkDrag to mat-row

onListDrop will look something like:

onListDrop(event: CdkDragDrop<string[]>) {
  // Swap the elements around
  moveItemInArray(this.myArray, event.previousIndex, event.currentIndex);
}

moveItemInArray is an Angular Material function. You can import it.

Sax answered 5/12, 2018 at 10:28 Comment(8)
Adding to this, don't forget to add DragDropModule to your NgModule as mentionned hereVentre
There's currently an issue, event.previousIndex only works the first time. A workaround is [cdkDragData]=row on mat-row, and previousIndex = this.myDataSource.findIndex(row => row === event.item.data)Planer
I got this far but I am having trouble adding a drag handle. Has anyone managed to make that work? Thanks!Antipyrine
I was able to get pretty far with the drag handle using the cdkrootelement suggestion from the github issue in the link. However scrolling with touch on the table does not work. Neither does auto scrolling as you drag an item to the edges of the screen.Antipyrine
@EthanMelamed can you show how you solved it with cdkrootelement? I can't find the issue you are mentioning.Finn
All I did was change cdkDrag -> cdkDragRootElement, and cdkDragHandle -> cdkDrag. I did notice some other issues with this approach so I would not recommend it. I ended up delivering my touch-friendly, angular table with draggable rows by adding an overlay on top of the table except for the area I wanted to be the “handle”. And I captured click events on the overlay and reinitialize the click on the mat-cell using document.elementsFromPoint() using the clientX&Y values from the origin click event.Antipyrine
You took the time to write this well thought out response. Next time might as well create a StackBlitz demonstrating a working sample. Would spare you all of these questions and provide a working solution for those trying to do this :)Inextinguishable
@Planer I spent a whole day searching for the reason why it didn't work and this one little comment from you saved my day! Thank you! 🙏Taveda
S
22

Found example https://stackblitz.com/edit/angular-igmugp

Looks the missing part is

this.table.renderRows();
Sheeting answered 24/1, 2019 at 16:8 Comment(2)
Is there any way to get currentIndex ?? Liek : const prevIndex = this.dataSource.findIndex((d) => d === event.item.data);Helvellyn
This sample works like a charm! Please pay attention to the cdkDrag [cdkDragData]="row" definition at the bottom of the table component... it actually what makes the rows draggableChapen
G
21

I was using MatTableDataSource for my dataSource so my solution was this:

  • Importing DragDropModule in component.module.ts

  • Importing CdkDragDrop in the component

  • Adding @ViewChild('table') table: MatTable<any>; to the component.ts.

  • In the HTML add:

    <table mat-table #table [dataSource]="dataSource" class="mat-elevation-z8"
      cdkDropList
      [cdkDropListData]="dataSource"
      (cdkDropListDropped)="drop($event)">
    
  • At the *matRowDef you need to add this :

    <tr mat-row *matRowDef="let row; columns: displayedColumns;"
      cdkDrag 
      [cdkDragData]=row>
    </tr>
    
  • Then in the component.ts I made the drop event:

    drop(event: CdkDragDrop<Scene[]>) {
      const previousIndex = this.dataSource.data.findIndex(row => row === event.item.data);
      moveItemInArray(this.dataSource.data,previousIndex, event.currentIndex);
      this.table.renderRows();
    }
    
Gorden answered 9/10, 2020 at 8:6 Comment(0)
B
11

I found a very good example on stackblitz: https://stackblitz.com/edit/table-drag-n-drop

To not break the ui of the Drag&Drop preview, use the tags

<mat-table>...</mat-table>
<mat-header-cell>...</mat-header-cell>
<mat-cell>...</mat-cell>
<mat-header-row>...</mat-header-row>
<mat-row>...</mat-row>

instead of

<table mat-table>...</table mat-table>
<th mat-header-cell>...</th mat-header-cell>
<td mat-cell>...</td mat-cell>
<tr mat-header-row>...</tr mat-header-row>
<tr mat-row>...</tr mat-row>

Combined with the css mentioned in https://mcmap.net/q/446139/-reorder-mat-table-rows-with-angular-material-39-s-drag-and-drop it is the perfect solution for me.

Brett answered 6/11, 2019 at 9:58 Comment(1)
I agree! Using <mat-table> and related tags + Lee Gunn's CSS works perfect!Grodin
C
3

If you are still using table to render a table instead of mat-table. You can consider the approach to manually set width of each td column on your table.

See the full explanation and stackblitz on https://trungk18.com/experience/angular-cdk-drag-drop-list-table/

.col-xs {
  width: 2%;
}

.col-sm {
  width: 10%;
}

.col-md {
  width: 20%;
}
<tbody cdkDropList (cdkDropListDropped)="onDrop($event)">
  <tr *ngFor="let user of users" cdkDrag cdkDragLockAxis="y">
    <th class="col-xs">
      <div class="drag-handle">
        <ng-container [ngTemplateOutlet]="dragHandleTmpl"> </ng-container>
        
      </div>
    </th>
    <td class="col-md"></td>
    <td class="col-md"></td>
    <td class="col-md"></td>
    <td class="col-md"></td>
  </tr>
</tbody>

This is the final result

Angular CDK Drag/Drop List inside table (not Material Table) - Handle rows distorting width

Cato answered 9/6, 2020 at 10:20 Comment(0)
E
0

Enhancement for pagniation.

I don't want to write an answer but not allowed to comment. I used the solution of @Lee Glenn & @Paul Ochon

If you use a table paginator you have to add the page offset to the currentIndex of the event to move element in array

const previousIndex = this.dataSource.data.findIndex(row => row === event.item.data);
const pageIndex: number = this.paginator?.pageIndex ?? 0;
const pageSize = this.paginator?.pageSize ?? 0
const currentIndex = event.currentIndex + ( pageSize * pageIndex);
moveItemInArray(this.dataSource.data,previousIndex,currentIndex);

this.dataSource.data = [...this.dataSource.data];
Engdahl answered 24/5 at 14:22 Comment(0)
D
-2

import clonedeep from 'lodash.clonedeep';


drop(event: CdkDragDrop<>) {
    console.log(event.previousIndex,event.currentIndex)
    moveItemInArray(this.dataSource.data,event.previousIndex, event.currentIndex);
    this.dataSource.data = clonedeep(this.dataSource.data);
}

This worked out for me

Disrespectable answered 13/12, 2022 at 18:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.