Angular CDK: Sorting Items using Drag & Drop

Originally published at labs.thisdot.co

Nov 23, 2021 · 6 minutes read · [Angular] [Angular CDK] [TypeScript]

There is no doubt that we are living in an era where the interactivity of your web application is a very important part of retaining your users.

For example, it’s very common to find requirements to perform a Drag and Drop operation inside of your web application: Upload image files, prioritize activities (think in a Task Board) or even sort elements in your web app.

Luckily, the Angular Team has implemented a library to enable the creating of drag-and-drop interfaces in your Angular application.

In this blog post, we’ll see a practical example to allow sorting Items using Drag and Drop considering a single and a mixed-orientation layout.

Project Setup

Prerequisites

You’ll need to have installed the following tools in your local environment:

  • Node.js. Preferably the latest LTS version.
  • A package manager. You can use either NPM or Yarn. This tutorial will use NPM.

Creating the Angular Project

Let’s create a project from scratch using the Angular CLI tool.

ng new angular-cdk-sorting-drag-drop --routing --prefix corp --style css --skip-tests

This command will initialize a base project using some configuration options:

  • --routing. It will create a routing module.
  • --prefix corp. It defines a prefix to be applied to the selectors for created components(corp in this case). The default value is app.
  • --style css. The file extension for the styling files.
  • --skip-tests. it avoids the generations of the .spec.ts files, which are used for testing.

Creating a Component

Before using the Angular CDK library, let’s create a component using the command ng generate as follows.

ng generate component dashboard

Now, pay attention to the output of the previous command, since it will auto-generate a couple of files:

CREATE src/app/dashboard/dashboard.component.css (0 bytes)
CREATE src/app/dashboard/dashboard.component.html (24 bytes)
CREATE src/app/dashboard/dashboard.component.ts (288 bytes)
UPDATE src/app/app.module.ts (487 bytes)

The last output line shows the affected module, where now the component belongs to.

Using Angular CDK

Install the Angular CDK library

The Angular Component Dev Kit (CDK) is a set of behavior primitives for building UI components. In fact, they’re used to build the widely used Angular Components.

Let’s start the package using NPM.

npm install --save @angular/cdk

Import the Drag&Drop Module

Before using any Drag&Drop feature from the Angular CDK library, it’s required to import the DragDropModule module from @angular/cdk/drag-drop, and then use it in the application module (in this case):

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DragDropModule } from '@angular/cdk/drag-drop';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';

@NgModule({
  declarations: [AppComponent, DashboardComponent],
  imports: [BrowserModule, AppRoutingModule, DragDropModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Sorting Items in a Single Orientation Layout

As you may find in the Drag and Drop documentation, you can set the orientation of any list using the property cdkDropListOrientation as the next example shows:

<div
  cdkDropList
  cdkDropListOrientation="horizontal"
  class="example-container"
  (cdkDropListDropped)="drop($event)"
>
  <div class="example-box" *ngFor="let timePeriod of timePeriods" cdkDrag>
    {{timePeriod}}
  </div>
</div>

If no orientation is configured, the directive cdkDropList will set vertical as the default orientation.

On other hand, the CSS code for the main container of that list could look like this:

example-container {
  display: flex;
  flex-direction: row;
}

Support on a Mixed Orientation Layout

You may assume that updating the CSS of the above main container and setting the flex-wrap: wrap property should be enough to support a mixed orientation layout. For example:

.example-container {
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
  gap: 10px;
  margin: 10px;
}

Saddly you may find some issues with the Drag & Drop behavior after applying those changes.

I had to deal with these problems recently in a project, and the good news is I found a helpful workaround that, at least on my side, is working fine.

Sorting Items in a flex-wrap Layout

One possible solution for supporting a mixed layout through flex-wrap: wrap is the use of a wrapper element via cdkDropListGroup directive. You may consider that any cdkDropList that is added under a group will be connected to all other lists automatically.

<div #dropListContainer class="example-container" cdkDropListGroup>
  <div
    *ngFor="let item of items; let i = index"
    cdkDropList
    [cdkDropListData]="i"
  >
    <div
      cdkDrag
      [cdkDragData]="i"
      (cdkDragEntered)="dragEntered($event)"
      (cdkDragMoved)="dragMoved($event)"
      (cdkDragDropped)="dragDropped($event)"
      class="example-box"
    >
      {{ item }}
    </div>
  </div>
</div>

Then, the associated TypeScript code for the previous template may need to consider an array of elements to generate the list:

// dashboard.component.ts

import { Component, ElementRef, ViewChild } from '@angular/core';

@Component({
  selector: 'corp-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.css'],
})
export class DashboardComponent {
  public items: Array<number> = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
}

Next, we’ll need to handle the cdkDragEntered, cdkDragMoved and cdkDragDropped events, which are related to the CdkDrag element(an item that can be moved inside a CdkDropList container).

// dashboard.component.ts

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

@Component({
  ...
})
export class DashboardComponent {
  @ViewChild('dropListContainer') dropListContainer?: ElementRef;

  dropListReceiverElement?: HTMLElement;
  dragDropInfo?: {
    dragIndex: number;
    dropIndex: number;
  };

  dragEntered(event: CdkDragEnter<number>) {
    const drag = event.item;
    const dropList = event.container;
    const dragIndex = drag.data;
    const dropIndex = dropList.data;

    this.dragDropInfo = { dragIndex, dropIndex };

    const phContainer = dropList.element.nativeElement;
    const phElement = phContainer.querySelector('.cdk-drag-placeholder');

    if (phElement) {
      phContainer.removeChild(phElement);
      phContainer.parentElement?.insertBefore(phElement, phContainer);

      moveItemInArray(this.items, dragIndex, dropIndex);
    }
  }

  dragMoved(event: CdkDragMove<number>) {
    if (!this.dropListContainer || !this.dragDropInfo) return;

    const placeholderElement =
      this.dropListContainer.nativeElement.querySelector(
        '.cdk-drag-placeholder'
      );

    const receiverElement =
      this.dragDropInfo.dragIndex > this.dragDropInfo.dropIndex
        ? placeholderElement?.nextElementSibling
        : placeholderElement?.previousElementSibling;

    if (!receiverElement) {
      return;
    }

    receiverElement.style.display = 'none';
    this.dropListReceiverElement = receiverElement;
  }

  dragDropped(event: CdkDragDrop<number>) {
    if (!this.dropListReceiverElement) {
      return;
    }

    this.dropListReceiverElement.style.removeProperty('display');
    this.dropListReceiverElement = undefined;
    this.dragDropInfo = undefined;
  }
}

Let’s explain when those methods are invoked:

  • The dragMoved method will be invoked once the user starts to drag an item. It’s expected to run it several times while being moved around the screen. In other words, the event will be fired for every pixel change in the position.
  • The dradEntered method will be invoked once the user has moved an item into a new container, which is, another cdkDropList element.
  • The dragDropped method will be invoked after the user drops the item inside a cdkDropList(container element).

One important aspect of this solution is the use of the cdkDragEntered event to identify when the user has moved an element in the screen. Also, in order to avoid a weird behavior of the .cdk-drag-placeholder element, there is a DOM manipulation for it just to make sure the current layout is not broken. Then, the data model is changed at the end through the moveItemInArray method, which is part of the CDK drag-drop API

Live Demo

Wanna play around with this code? Just open the Stackblitz editor:

A Simplified Version

If you’re curious about a simplified workaround, you can take a look at this demo, which is the initial version for this solution. Keep in mind it doesn’t include the fix for the .cdk-drag-placeholder element handling I described above.

Source Code of the Project

Find the complete project in this GitHub repository: angular-cdk-sorting-drag-drop. Do not forget to give it a star ⭐️ and play around with the code.


Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

tweet Share