31

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?

Mel
  • 5,837
  • 10
  • 37
  • 42
David Brem
  • 504
  • 2
  • 6
  • 16
  • 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. – jet miller Nov 27 '18 at 18:23

6 Answers6

34

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.

Lee Gunn
  • 8,417
  • 4
  • 38
  • 33
  • 2
    Adding to this, don't forget to add `DragDropModule` to your NgModule as mentionned [here](https://material.angular.io/cdk/drag-drop/overview#getting-started) – Antoine Delia Jan 31 '19 at 09:03
  • 7
    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)` – Mel Sep 13 '19 at 09:20
  • I got this far but I am having trouble adding a drag handle. Has anyone managed to make that work? Thanks! – Ethan Melamed Sep 19 '19 at 18:44
  • 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. – Ethan Melamed Sep 20 '19 at 17:51
  • @EthanMelamed can you show how you solved it with cdkrootelement? I can't find the issue you are mentioning. – Roger Far Sep 30 '19 at 00:55
  • 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. – Ethan Melamed Sep 30 '19 at 13:57
  • 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 :) – Francisco Aguilera Feb 17 '20 at 14:59
  • @Mel 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! – Wilt Jun 18 '21 at 06:40
21

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

Looks the missing part is

this.table.renderRows();
  • Is there any way to get **currentIndex** ?? Liek : **const prevIndex = this.dataSource.findIndex((d) => d === event.item.data);** – Anzil khaN Aug 02 '19 at 11:22
  • 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 draggable – ymz Sep 27 '19 at 14:12
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();
    }
    
Valentin Bossi
  • 186
  • 2
  • 9
Paul Ochon
  • 311
  • 2
  • 3
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://stackoverflow.com/a/53630171/12331146 it is the perfect solution for me.

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

trungk18
  • 19,744
  • 8
  • 48
  • 83
-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