Drag-and-drop with Angular Material

Drag-and-drop with Angular Material

by Godwin Chinda

Angular Material is a User Interface (UI) component library that developers can use to quickly create stunning and cohesive user interfaces in their Angular projects. With Angular Material, you can find reusable and appealing 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 Angular Material components. The available list of Angular Material components grows with each library iteration.

Drag and drop is a platform where applications use drag-and-drop functionality on browsers. The user clicks and drags files to a droppable element (drop zone) using a mouse or touchpad, then releases the mouse button to release the files. 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.

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.

1

Creating a Drop Zone

The next step 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 drop zone if we attempt to dump it outside the drop zone.

<div cdkDropList>
  <div class="box" cdkDrag>
    Drag me around
  </div>
</div>

2

Re-Ordering Items Inside a List

The next stage is dragging and re-ordering things inside a list after learning to make a draggable item and a drop zone. 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 a string array 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. When we drag and drop anything, it returns to its original place.

3

We must implement the cdkDropDropped method in order 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 shown in the code above. 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>

This results in elements inside the cdkDrop container being draggable and reorderable. Click here for a better understanding.

4

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.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Dragging from one list to another list

Let's go one step farther and construct a straightforward 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 the availableGoods list;
  • add the availableGoods to the incomingGoods and soldGoods list;
  • add the soldGoods list onto the availableGoods;

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; to meet our needs, it must transfer items from one list to another list.

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
    );
  }
}

It rearranges the elements in the same previous order if the container is the same. The dragged item gets transferred to the list where it is being dumped if the container differs. Once more, a useful function called transferArrayItem is included right out of the box. Check can the documentation.

5

Disable dragging

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 we set the boolean function 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.

6

Conclusion

Angular Material is flat and extremely simple by design. It is made with the understanding that adding new CSS rules is considerably simpler than changing already-existing CSS rules. This is simple and intuitive to use, but it also offers flexibility by allowing you to take charge when needed.

You can get more information by consulting the sourcecode and documentation.

Important resources:

newsletter