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 isapp
.--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, anothercdkDropList
element. - The
dragDropped
method will be invoked after the user drops the item inside acdkDropList
(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.