Angular Drag and Drop

Angular Material Drag and Drop – Strengths and Limitations

Rachel Walker Angular, Development Technologies, Programming Leave a Comment

When Drag and Drop was introduced to the Angular Material/CDK in version 7, it promised to support free dragging, interactive lists, and other common drag and drop operations without third-party library dependencies. Since that initial release, it has received consistent updates to further that goal.

In this blog post, I will be exploring some of the strengths and limitations of the Module that I encountered while implementing both simple and complex drag and drop functionality with CDK version 13.3.5.

Getting Started with Arrays

Getting started with Material Drag and Drop is very simple.

In an existing angular application with the CDK installed, import the DragDropModule from @angular/cdk/drag-drop into the appropriate NgModule.

Once the module is imported, the cdkDropList and cdkDrag directives specify the drop zone and draggable elements. The module also exposes several functions to handle sorting within an array. The following snippet is all that is required to render a list of items with a drag handle and allow sorting within the Array.

import { Component} from '@angular/core';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';

@Component({
  selector: 'drag-and-drop',
  template: `
    <div cdkDropList (cdkDropListDropped)="drop($event)">;
      <div *ngFor="let item of itemArray" cdkDrag>
        <button mat-icon-button type="button" cdkDragHandle>
          <mat-icon>reorder</mat-icon>
        </button>
        {{item}}
      </div>
    </div>
})
export class DragAndDropComponent {
  itemArray = [
    'Item 1',
    'Item 2',
    'Item 3'
  ];

  drop(event: CdkDragDrop<string[]>) {
    moveItemInArray(this.itemArray, event.previousIndex, event.currentIndex);
  }
}

The CDK documentation provides clear examples of using the API to add drag handles, custom placeholders, previews, and support for multiple drop lists. Those examples are good for getting started, but there are a few design features of the Module that extend the functionality well beyond just reordering string arrays.

Enforcing Business Rules

Both the cdkDropList and the cdkDrag allow arbitrary data to be attached, which is passed back in their events. This data is directly attached and is not modified. I found this particularly helpful when the drop event resulted in a service call, such as moving data to the next page of a result set.  Since the data was in the event context, it was the same amount of code to wait until the service call succeeded to update the component or to update the component and then roll back on failure.

Preventing failure is always preferable to rolling back. To facilitate this, the API accepts a variety of predicates to enforce when drop lists are valid. The cdkDropListSortPredicate in particular, which was added in v11, is extremely helpful for modifications within a single drop zone.

The predicates are functioned triggered prior to Entering/Exiting/Sorting events and return a boolean to indicate if the event should proceed. The data attached to the cdkDrag directive is in context during the predicate check, but they cannot access the state of the containing component. They work well when the attached data is sufficient to determine if the drop zone is a valid destination for the draggable item.

In more complex use cases, this attached data may not be sufficient to make that determination unless wrapper classes are created. The cdkDropListConnectedTo provides a different alternative to enforce those more advanced conditions.

By default, draggable items cannot be moved outside of their cdkDropList of origin. To move to a different destination, both the source and destination drop lists must be placed within a common cdkDropListGroup, which will associate them as siblings.

Alternatively, the lists can be placed in separate cdkDropListGroups, and a list of connected drop lists can be specified with the cdkDropListConnectedTo input. When the CdkDragExit event is triggered, the event handler can check the appropriate component state as well as the attached drag data, then update the connected drop lists so that it only contains valid destinations.

Multi-Dimensional Arrays

The Drag and Drop Module has strong support for single orientation Arrays, but unfortunately, when switching to multiple orientations, significant limitations quickly arise. By default, cdkDropList uses a vertical orientation. However, a horizontal orientation can be specified instead. This leaves a large gap when it comes to Grids, Tables, and other use cases that require both horizontal and vertical sorting. This has been an open issue since 2018.

The most common accepted workaround for this issue leverages the behavior of the cdkDropListGroup by wrapping each item in its own cdkDropList. This triggers the enter and exit predicates, which provide hooks for custom logic. While this is sufficient for some use cases, it is more complex to handle each item as an array and can lead to additional manual styling and element manipulation. It is a large functional gap for a native drag and drop module.

In addition to the general complexity, wrapping each element in its own list can also expose another outstanding issue. When drop zones are nested, such as in a list with children elements, the outer drop zone is selected rather than the inner drop zone. There are two separate contributing factors to this problem.

First, when determining which drop zone the pointer is positioned in, the library traverses the dom until it finds an overlapping element. The cdkDropListGroup adds the parent first, followed by children when the connected drop lists are not specified. To avoid this issue when using nested drop lists, the connected drop lists can be ordered so that the inner components are added first, followed by the outer components. This can become complicated when there is deep nesting and may still result in initial snapping to the wrong element.

The second contributing factor is internal position detection within the module. It does not consistently detect the correct sibling. This will require a patch to definitively correct, although there is an unofficial “monkey patch” provided by the community on the issue.

My summary here has been very high level. Both of these issues have comprehensive community discussion and multiple proposed solutions on the Github issues. They are worth the time to read and understand when trying to evaluate if it is worth working around the issues or if switching libraries completely is the smartest choice.

In my recent application, I encountered the problem I mentioned with nested lists. Because I was working in a recursive data structure that allowed reordering of the elements, I had already written a service to manage those updates.

As I was researching options, I ran across Ernestas Buta’s Angular CDK nested Drag And Drop and determined that enhancing my existing service to order and register the drop lists would likely be sufficient for my use case. I did not install any patches to correct the zone detection logic. Instead, I subscribed to the cdkDropList events and used them to modify which drop lists were connected. This worked with my UI layout, but it wouldn’t be feasible in every use case.

Position Caching

As I was experimenting with different structures for nested lists, I quickly noticed that dynamic height changes were not being detected if they were made after the drag action began.

The most basic use case is wanting to show different placeholders for each drop zone. In the CDK Drag and Drop Module, the placeholder is associated with the drag item rather than the destination drop list. So, I tried subscribing to the zone entry and exit events and adding the appropriate classes as each drop zone came into context. The new zones appeared, but the drag placeholder suddenly began appearing in the wrong place.

After some investigation, I learned when the drag event starts, the positions of all the drop lists are cached in the module. There is not a publicly exposed method to refresh the cache, so any element height or position changes after that caching will cause the Module to incorrectly calculate the drop position. Updating the styling on the CSS classes used by the module is sometimes effective as an alternative, but not always. Many of these classes are set by internal host binding, so these changes will not be picked up until change detection runs. There are multiple open issues with this same underlying cause.

In my use case, I was able to tweak the design so that the drop zones were a consistent height. I was able to update the internal text and use CSS styling for sufficient visual differences for my use case. This required significant time to work through and some time spent reading the Module source code to understand. As I was investigating, I noticed that this same underlying caching approach is also one of the contributing factors to the zone detection issues with nested lists.

All in all…

The Angular CDK Drag and Drop Module is simple to add to a new or existing application and excels in straightforward array manipulation and dragging items with a flat structure and static styling. The API documentation and examples are sufficient to get started quickly. It is flexible enough for custom components and does not introduce extra state management for developers. For these cases, it is sufficient to use rather than a third-party library.

However, when I began adding more complex use cases, I entered a web of workarounds for what seemed to be common drag-and-drop situations. The robust community participation was very helpful in determining when I was structuring things incorrectly or if there was an error in the library.

Overall, the Angular Material CDK Drag and Drop module has come a long way since the initial release, but in applications with nested components or dynamic rendering, I recommend continuing to evaluate using a third-party drag and drop library until these core issues are resolved.

Let me know in the comments below what you think of Material Drag and Drop. Have you found any workarounds of your own? I’d love to know.

If you liked this post and want to learn more, check out the Keyhole Dev Blog. All posts are written by Keyhole Consultants like myself.

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments