7
votes

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

7

7 Answers

2
votes

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

9
votes

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
>
2
votes

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.

2
votes

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.

1
votes

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.

0
votes

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.

0
votes

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.