Drag-and-drop with Angular Material
Angular Material is a UI component library that developers can use to quickly create stunning and cohesive user interfaces in their projects. Within Angular Material, you can find reusable UI components like Cards, Inputs, Data Tables, Datepickers, and much more. Each component is ready to be used in the default style according to the Material Design specification. Still, you can easily customize the look of these components. The available list of Angular Material components grows with each library iteration.
With drag and drop users can click and drag files to a droppable element (drop zone) using a mouse or touchpad, then by releasing the mouse
button they “drop” the files. This allows you to build very intuitive user experiences relatively easy.
Angular Drag and Drop CDK (Component Dev Kit) provides support for free dragging,
sorting within a list, moving items between lists, animations, touch devices, custom drag handles, and more. The @angular/cdk/drag-drop
module also allows you to create drag-and-drop interfaces quickly and declaratively.
And that is what we’re going to be doind in this article, so keep on reading!
Getting Started
The first step is to install Angular Material UI into our project.
ng add @angular/material
The DragDropModule
will be imported into NgModule
:
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {DragDropModule} from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
DragDropModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Now that we’ve imported the module, we can create our first draggable component by using the cdkDrag
directive.
<div class="box" cdkDrag>
Drag me around
</div>
We can already drag and drop in our project after running the code.
Creating a Drop Zone
The next thing is to build a drop zone now that we know how to drag an element. We’ll utilize the new directive cdkDropList
to do this; it will serve as a container to dump the draggable elements into. The item will return to its original location inside the draging zone if we attempt to dump it outside the drop zone.
<div cdkDropList>
<div class="box" cdkDrag>
Drag me around
</div>
</div>
Re-Ordering Items Inside a List
Now that we know how to drag elements around and how to create a “drop zone” to leave things in, we’ll tackle “re-ordering”. To generate the list components inside a cdkDrop
container, we’ll utilize the *ngFor
directive.
<div class="box" cdkDropList>
<div *ngFor="let item of items" cdkDrag>{{item}}</div>
</div>
The elements are defined as an array of strings in the AppComponent
:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
items = ['Football', 'Tennis', 'Basketball', 'Rugby', 'Golf']
}
The GIF below shows how things automatically rearrange themselves as we drag them.
You’ll notice that when we drag and drop anything, it returns to its original place, we need to fix that!
We must implement the cdkDropDropped
method to resolve this issue and save the new index when an item is dropped inside the list. When a user drops something inside the drop zone, the dropped function is always called. Its trademark is as follows:
import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop';
@Component({...})
export class AppComponent {
title = 'dropzone';
items = [...]
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
}
}
The utility function moveItemInArray
is also included in the drag and drop CDK, as you already saw before. The array’s dropped item’s new index is determined using this function, rad! It’s time to link the dropped
function to the HTML’s cdkDrop
element now that we have implemented it.
<div class="box" cdkDropList
(cdkDropListDropped)="drop($event)">
<div *ngFor="let item of items" cdkDrag>{{item}}</div>
</div>
Now elements inside the cdkDrop
container are draggable and reorderable (and they don’t fly back to the wrong place when we drop them!). Click here for a better understanding.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Dragging from one list to another list
Let’s go one step farther now and construct a simple task board. To do this, we will divide the items
array into three smaller arrays: one for new items, one for active items, and one for completed items.
incomingGoods = ['Tomatoes', 'Carrots', 'Onions', 'Pepper']
availableGoods = ['Cucumber']
soldGoods = ['Orange', 'Apple', 'Banana']
Three different lists must be displayed, and each list will have its own drop zone. By using the cdkDropData
input, we can connect the arrays to a drop zone.
<div
cdkDrop
#new="cdkDrop"
[cdkDropData]="newItems"
[cdkDropConnectedTo]="[active]"
(cdkDropDropped)="dropped($event)"
>
<div *ngFor="let item of newItems" cdkDrag>{{ item }}</div>
</div>
A cdkDrop
list instance can be linked to another cdkDrop
list instance using the [cdkDropConnectedTo]
input attribute. We won’t be able to drag and drop the things to another list if we don’t do this. The connections that need to be made in our task board example are as follows:
- add the
incomingGoods
to theavailableGoods
list; - add the
availableGoods
to theincomingGoods
andsoldGoods
list; - add the
soldGoods
list onto theavailableGoods
;
To put it another way, you can drag a incomingGoods
to the availableGoods
, the soldGoods
, or the reverse order. However, you must first move through the availableGoods
to drag a incomingGoods
to the soldGoods
. Combining these results yields the following:
<div cdkDropListGroup>
<div class="container">
<h2>Incoming Goods</h2>
<div
id="incoming"
cdkDropList
[cdkDropListData]="incomingItems"
cdkDropListConnectedTo="available"
class="list"
(cdkDropListDropped)="drop($event)"
[cdkDropListEnterPredicate]="noReturnPredicate">
<div class="box" *ngFor="let item of incomingItems" cdkDrag>{{item}}</div>
</div>
</div>
<div class="container">
<h2>Available Goods</h2>
<div
id="available"
cdkDropList
[cdkDropListData]="availableItems"
cdkDropListConnectedTo="sold"
class="list"
(cdkDropListDropped)="drop($event)"
>
<div class="box" *ngFor="let item of availableItems" cdkDrag>{{item}}</div>
</div>
</div>
<div class="container">
<h2>Sold Out Goods</h2>
<div
id="sold"
cdkDropList
[cdkDropListData]="soldItems"
cdkDropListConnectedTo="available"
class="list"
(cdkDropListDropped)="drop($event)"
>
<div class="box" *ngFor="let item of soldItems" cdkDrag>{{item}}</div>
</div>
</div>
</div>
Making our dropped
function smarter is the final phase; we need to transfer items from one list to the other. So let’s take a look at what the code for that would look like:
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
dropped(event: CdkDragDrop<string[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(
event.container.data,
event.previousIndex,
event.currentIndex
);
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
}
When drag & dropping an element, you have two options: you either do it within the same list or into another list.
If you’re dropping the elementin inside the same list, It rearranges the elements. If on the other hand, you’re moving items from one list to the other, then the dragged item gets transferred to the list where it is being dumped. Once more, a useful function called transferArrayItem
is included right out of the box. You can check the documentation to know more.
Disable dragging
If you want to disable the ability to drag elements around, simply setting the cdkDragDisabled
input on a cdkDrag
item will disable dragging for that particular drag item. Additionally, you can disable a specific handle using the cdkDragHandleDisabled
input on a cdkDragHandle
or an entire list using the cdkDropListDisabled
input on a cdkDropList
.
<div cdkDropList class="list" (cdkDropListDropped)="drop($event)">
<div
class="box"
*ngFor="let item of items"
cdkDrag
[cdkDragDisabled]="item.disabled">{{item.value}}</div>
</div>
In our app.component.ts
file, we set the boolean property disabled
of our value to either true/false.
import {Component} from '@angular/core';
import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
items = [
{value: 'Oranges', disabled: false},
{value: 'Bananas', disabled: true},
{value: 'Mangoes', disabled: false},
];
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
}
Our value ‘Banana’ is set disabled:true
, which invokes the cdkDragDisabled
to disable dragging for that particular item. Click here for more information.
Conclusion
Angular Material is flat and extremely simple by design. It was designed for you to add new CSS rules instead of changing the existing ones. In theory, this design philosophy is meant to be simple and intuitive to use, and at the same time, offer a great deal of flexibility by allowing you to take charge when needed.
You can get more information by consulting the sourcecode and documentation.
Important resources:
A TIP FROM THE EDITOR: If you enjoyed this article, you should also take a look at Getting started with Angular Material UI for more information about getting started with AngularMaterial.