How to make a mat-table row drag-drop work with cdkDragHandle so that the row is only draggable using the handle?
Asked Answered
K

7

10

I found this stackblitz example of adding drag-drop to a mat-table using angular cdk. However, the desired behavior is that the row is only draggable using the element with the cdkDragHandle directive. In this example you can drag the element by clicking anywhere on the row. How can this be modified so that the row is only draggable using the drag handle?

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

Keratosis answered 6/3, 2019 at 16:53 Comment(1)
Answered in #53308111.Visual
B
3

I have found a somewhat simple issue to this complex problem. For any simple text td in the draggable tr, we can use the pointer-events:none and it will disable all the text element.

On the handle icon, use the pointer-events:all and it will enable dragging from only the icon.

This also has the issue where it disables all the anchor and buttons. So for icon and buttons do the follwoing

  1. using mouseDown set a flag
  2. on drag start, check the drag and throw mouseup event
  3. on drag stop, check if flag is set and reset flag and return

check this stackblits for working answer https://stackblitz.com/edit/angular-rwzc76

Bushwhacker answered 11/7, 2019 at 0:31 Comment(3)
I ended up going a different route and just having a separate reorder view that is toggled on/off. But this seems good to me, nice!Keratosis
I ended up having two different views, and only one is shown at a time. So at the top of the page I have a toggle that turns on "reorder mode", and when reorder mode is toggle on, then the mat-table is hidden, and a separate list of items is displayed that are drag-droppable.Hermaphroditism
Something like this but styled better: stackblitz.com/edit/angular-pjg922 Btw it's phelhe i changed my name to gibberishHermaphroditism
B
15

Here is my workaround for this issue:

  1. Make a boolean to control whether cdkDrag should be disabled. Default behavior is disabled.
  2. Add a mousedown, mouseup, touchstart, and touchend event handler to the cdkDragHandle to toggle the control.
  3. In the cdkDrag, listen to the cdkDragReleased event to disable the cdkDrag after it is dragged.

The side-effect is that it becomes harder to work with items that you really want to disable (e.g. apply style for those truly disabled items).

The code looks like below:

  • Component class
  dragDisabled = true;
  • cdkDrag
<mat-row
  *matRowDef="let row; columns: displayedColumns"
  cdkDrag
  [cdkDragData]="row"
  [cdkDragDisabled]="dragDisabled"
  (cdkDragReleased)="dragDisabled = true"
></mat-row>
  • cdkDragHandle
<mat-icon
  cdkDragHandle
  (touchstart)="dragDisabled = false"
  (touchend)="dragDisabled = true"
  (mousedown)="dragDisabled = false"
  (mouseup)="dragDisabled = true"
  >drag_indicator</mat-icon
>
Bathulda answered 12/5, 2020 at 3:52 Comment(0)
U
4

I have achieved that UX by applying cdkDrag to the dragHandle itself, instead of the row, and using cdkDragRootElement to identify the row. It achieves the UX of dragging via handle, but there's still a bug that prevents the actual reordering after the drop.

See Stackblitz here.

Documentation of cdkDragRootElement is here.

Here is link to Github issue about it.

Uninterested answered 26/4, 2019 at 13:3 Comment(3)
You example broke the sorting function. Is that intentional?Ornament
As you can see from what I wrote, there is still a known bug with the actual reordering. All my solution does, is allow dragging by a drag handle. This is unfortunately an incomplete solution, we are still waiting for CDK to fix the buggy Drag/Drop implementation.Uninterested
IMPORTANT! The Github issue was closed on May 25, 2020. @Uninterested has the proper solution since the issue has been resolved in recent Angular versions.Bucaramanga
B
3

I have found a somewhat simple issue to this complex problem. For any simple text td in the draggable tr, we can use the pointer-events:none and it will disable all the text element.

On the handle icon, use the pointer-events:all and it will enable dragging from only the icon.

This also has the issue where it disables all the anchor and buttons. So for icon and buttons do the follwoing

  1. using mouseDown set a flag
  2. on drag start, check the drag and throw mouseup event
  3. on drag stop, check if flag is set and reset flag and return

check this stackblits for working answer https://stackblitz.com/edit/angular-rwzc76

Bushwhacker answered 11/7, 2019 at 0:31 Comment(3)
I ended up going a different route and just having a separate reorder view that is toggled on/off. But this seems good to me, nice!Keratosis
I ended up having two different views, and only one is shown at a time. So at the top of the page I have a toggle that turns on "reorder mode", and when reorder mode is toggle on, then the mat-table is hidden, and a separate list of items is displayed that are drag-droppable.Hermaphroditism
Something like this but styled better: stackblitz.com/edit/angular-pjg922 Btw it's phelhe i changed my name to gibberishHermaphroditism
N
2

IMHO there is no quick-fix to this, other than hacking/overriding the source code of Angular Material / CDK. Testament of this is the open feature request at github: https://github.com/angular/material2/issues/13770.

The issue is that the cdkDrag on a datasource / MatTable automatically creates drag annotations on all child elements (which generates the behavior) and can't be (easily) overriden.

Based on the documentation cdkDrag/cdkDragDisabled - cdkDragHandle/cdkDragHandleDisabled should help (it does work without a table). I've upgraded all the libraries from the example to support them but to no effect.

Nahuatl answered 6/3, 2019 at 18:35 Comment(0)
Y
2

Here an example to start to drag a row only by specific column:

Create a variable on component

dragEnabled = false;

Set the row as draggable and disable drag by the variable

<mat-row *matRowDef="let row; columns: columns;" cdkDrag [cdkDragDisabled]="!dragEnabled">
</mat-row>

Control the dragEnabled variable state by mouse events on specific column

<ng-container matColumnDef="drag">
  <mat-header-cell *matHeaderCellDef>
  </mat-header-cell>
  <mat-cell *matCellDef="let entity"
            (mouseenter)="dragEnabled = true"
            (mouseleave)="dragEnabled = false">
    <mat-icon>
      drag_indicator
    </mat-icon>
  </mat-cell>
</ng-container>

Now you can select row contents and drag the row only by a specific column.

You answered 21/7, 2021 at 14:54 Comment(1)
works perfectly and super simple. great.Kareykari
H
1

I have an alternative answer here (which perhaps was not possible previously) - it combines @H Dog and @Mamoon ur Rasheed's answers.

As per H Dog's answer, move the drag handle into the cell itself instead of the row, and use cdkDragRootElement to select the parent mat-row. However, this still leaves the full row as draggable.

Next, disable drag by default, bound to a boolean on the component. When a mousedown event is fired on the drag handle, enable drag and then, in the next frame, disable it again.

This leaves the full row as allowing normal interaction, but enables dragging via the drag handle with proper placeholder and preview functionality.

Herries answered 3/2, 2021 at 15:53 Comment(0)
H
0

I have an alternate solution to the ones posted here. I had a requirement beyond just links and plain text cells that required more interactivity (including text selection, input fields, etc).

Using a directive on any non-draggable cells that will be rendered in a cdkDrag table row (tr), I was able to stop the mousedown event from bubbling down to the active cdkDrag row of the cdkDropList instance.

Here is what my directive eventually ended up looking like.

import {Directive, ElementRef, OnDestroy, OnInit} from '@angular/core';

@Directive({
  selector: '[appCancelCdkDrag]'
})
export class CancelCdkDrag implements OnInit, OnDestroy {
  $element: HTMLElement;

  constructor(el: ElementRef) {
    this.$element = el.nativeElement;
  }

  fireMouseUp($event: MouseEvent) {
    $event.cancelBubble = true;
  }

  ngOnDestroy(): void {
    this.$element.removeEventListener('mousedown', this.fireMouseUp);
  }

  ngOnInit(): void {
    this.$element.addEventListener('mousedown', this.fireMouseUp);
  }

}

StackBlitz here: https://stackblitz.com/edit/angular-tgrcni

Here is a related comment on the Github Angular Components page: https://github.com/angular/components/issues/13770#issuecomment-553193486

Hope this helps.

Holdall answered 13/11, 2019 at 1:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.