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?
7 Answers
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
- using mouseDown set a flag
- on drag start, check the drag and throw mouseup event
- 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
Here is my workaround for this issue:
- Make a boolean to control whether
cdkDrag
should be disabled. Default behavior is disabled. - Add a
mousedown
,mouseup
,touchstart
, andtouchend
event handler to thecdkDragHandle
to toggle the control. - In the
cdkDrag
, listen to thecdkDragReleased
event to disable thecdkDrag
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
>
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.
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.
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.
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.
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.