Drag and Drop in Angular

Jeffry Houser
disney-streaming
Published in
13 min readMar 29, 2019

I’m Jeffry Houser, a developer in the content engineering group at Disney Streaming Services. My team builds content portals that allow editors to create magic for our customers in the video services we power.

This article will teach you to implement drag and drop in an Angular application. Having worked on web applications for so long, drag and drop scares me. As a user interface mechanism, it is not easily discoverable, and I worry about it confusing users. In the early days, browser support was inconsistent at best and adding such advanced UI touches was more pain than it was worth. These days, things have changed! Frameworks encapsulate away a lot of browser incompatibility issues. When we needed to support the reordering of a list of items, implementing drag and drop was a great approach.

I’m going to show you how we used the Dragula library to implement Drag and Drop in our application.

Setup Your Angular Project

To follow these instructions, you’ll need to create an empty Angular project. If you don’t have it, I’d start by installing the Angular CLI using these instructions. The next step is to create the project with this command:

ng new

You’ll see something like this:

I created this project using Angular 7. It is named `DragAndDrop03`, the 03 being part of my internal naming structure, and you can name your project whatever you like. I also enabled routing and used CSS for this sample. We probably won’t need routing, but I add it as a force of habit.

Now install Dragula, the drag and drop library:

npm install ng2-dragula

You’ll see this:

Open up the polyfill.ts file from the src directory of your project and add this code at the bottom:

(window as any).global = window;

This is required for the current version of Dragula to work. We’ll want to be sure to include the Dragula styles. Open up the angular.json file. Find the styles section, like this:

"styles": [
"src/styles.css"
],

Add the styles from the Dragula library:

"styles": [
"src/styles.css",
"node_modules/dragula/dist/dragula.css"
],

Be sure to load the DragulaModule into the app.module.ts. First, import the library:

import { DragulaModule } from 'ng2-dragula';

And then load it as part of the imports section of the NgModule metadata:

imports: [
BrowserModule,
AppRoutingModule,
DragulaModule.forRoot()
],

Now we’re are ready to add some drag and drop to our app.

Set up Drag and Drop

The first step is to create a data source of items we want to be able to drag and drop. Open up the app.component.ts file. I created an array of character objects, representing made up characters, and of course me:

characters = [
{id: 1, firstName: `Jeffry`, lastName: 'Houser'},
{id: 2, firstName: `Annie`, lastName: 'Anderson'},
{id: 3, firstName: `Bob`, lastName: 'Boberson'},
{id: 4, firstName: `Candy`, lastName: 'Canderson'},
{id: 5, firstName: `Davey`, lastName: 'Daverson'}
]

For my test purposes, I added a total of 27 characters. Open up the app.component.html and delete all the contents inside it.

I’m going to add a div to wrap all items here:

<div class="wrapper">
</div>

Inside of app.component.css, put some CSS for this div:

.wrapper {
width: 100%;
height: 100%;
text-align: center
}

This just expands the wrapper to the full height and width of the page, while also aligning all it’s children to the center of the screen.

How create an embedded container — this will be our Dragula container:

<div class="dragula_container">
</div>

Create some CSS for this div:

.dragula_container {
height: 100%;
width: 200px;
display: inline-block
}

The height is `100%` to expand the full height of the screen; the width is `200px` so that it will be centered. If we expanded it to `100%` width it would look left aligned. The CSS display is `inline-block` so that each character will be on their own separate line.

Finally, create the characters by looping inside the Dragula container:

<div *ngFor="let character of characters">
<div class="character">
{{character.firstName}} {{character.lastName}}
</div>
</div>

This creates a div with the character details on it. Add some CSS to make things look nice:

.character {
padding: 10px;
background: white;
margin: 5px;
border: grey solid 2px;
width: 100%
}

Primarily all this CSS design is for elegance of the demo and is unrelated to drag and drop specifics. Run your app, and you should see something like this:

Now we need to tell Dragula about this container so it can allow for drag and drop. In the HTML template, find the dragula_container div:

<div class="dragula_container" 
dragula="DRAGULA_CONTAINER"
[(dragulaModel)]="characters">

We added two items to this. The first is the dragula property. This property’s value is how we separate different Dragula groups, in case we have multiple sections of the screen that allow for drag and drop.

Now switch back to the app.component.ts. Inject the DragulaService:

constructor(private dragulaService: DragulaService) {}

You’ll need to import this too:

import {DragulaService} from 'ng2-dragula';

Then, create the drag and drop group:

ngOnInit() {
this.dragulaService.createGroup('DRAGULA_CONTAINER', {});
}

Be sure to import the OnInit interface:

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

And be sure that your AppComponent class implements it:

export class AppComponent implements OnInit {

That is all you need to do to implement drag and drop with Dragula in an Angular app. Rerun your code and test it out:

Set up Auto Scrolling

If you play around with the demo for any length of time, you’ll notice that the list of items does not automatically scroll when you drag to the edge of the screen. We are going to solve that by plugging in a library, named dom-autoscroller. First install it:

npm install dom-autoscroller

You’ll see something like this:

We’re going to want to make a few changes to the styles. First open up the styles.css in the src directory:

html, body {
height: 100%;
}

This will prevent the HTML page from extending off the page. For scrolling to work it needs to know where its limits are.

Open up the app.component.html file and find the first few lines:

<div class=”wrapper”>
<div class=”dragula_container”
dragula=”DRAGULA_CONTAINER”
[(dragulaModel)]=characters>
<div *ngFor=”let character of characters>

The structure here is important. The wrapper layer is the scrollable layer. The dragula_container div sets up the Dragula library. The third embedded div is the loop that defines all the individual scrollable elements. We’re going to need to access the scrollable div as a ViewChild, so add the #autoscroll to it:

<div #autoscroll class=”wrapper”>

And within the app.component.ts class, add this code:

@ViewChild(‘autoscroll’) autoscroll: ElementRef;

The ViewChild metadata gives us a programmatical hook into the HTML div.

We’re going to make one style tweak to the wrapper CSS inside the app.component.css:

.wrapper {
width: 100%;
height: 100%;
text-align: center;
overflow-y: scroll;
}

I added the overflow-y property and set it to `scroll` so that it would always be scrollable. You’ll probably have acceptable results if you set it to `auto`.

Go back to the app.component.ts file to import the scroller:

import autoScroll from ‘dom-autoscroller’;

Now go to the ngOnInit() method to set up scrolling:

autoScroll([
this.autoscroll.nativeElement
], {
margin: 35,
maxSpeed: 4,
scrollWhenOutside: true,
autoScroll() {
return this.down;
}
});

The dom-autoscroll library is not built as a native Angular library, so it feels a bit weird the way we use it. But, after the import we call a function, pass in an array of the element we want it to watch. In this case, just our autoscroll ViewChild’s nativeElement. The second argument is a parameter object. I included a few common parameters. The maxSpeed is the speed which it moves. ScrollWhenOutside will scroll when you are dragging an item outside of the container. The margin is how close to the edge of the container you want to be when scrolling starts. The autoScroll() function tells us to only autoScroll when the mouse button is held down.

Rerun your code, and you should see an image like this:

I wish the drag and scrolling was native to the library, but thankfully the dom-autoscroller is easy to plugin and setup.

Dragula API Overview

Now that I’ve shown you a simple sample of dragging and dropping, I want to review the full API. Dragula includes a full featured API that allows you to tweak how drag and drop works for your application. First let’s look at some options in the configuration object:

  • isContainer: This function determines if we can perform drag and drop operations on the container in question. Usually I do not use this, preferring to tell Dragula explicitly what containers are Dragula containers using the directives.
  • moves: This is a function that executes whenever an item is clicked on. It determines whether an item can be moved or not.
  • accepts: This function checks to see whether an element can be dropped in the specified container or not.
  • invalid: This function can be used to prevent a drag operation from starting.
  • direction: This property is either `vertical` or `horizontal`, with `vertical` being the default. When dropping an item, this property determines which axis should be considered when an item is dropped.
  • copy: If this property is set to `true`, then elements are copied from one list to another. If set to `false`, the elements are moved. The default is `false`.
  • copyItem: This is a function that will return a copy of the object being copied. It is most useful if you have a nested object structure and need to write custom code for the copying.
  • copySortSource: When copying from one container to another, this property determines if elements in the source container can be reordered. The default is `false`.
  • revertOnSpill: When you drop an item out of the container, if this property is set to `true` then the item will go back to its place in the source list.
  • removeOnSpill: When you drop an item out of the container, if this property is set to `true` then the item will be deleted from the source list.
  • mirrorContainer: This is the container where elements will be shown while you are dragging them. The default is the `document.body`.
  • ignoreInputTextSelect: If this property is set to true, then the drag will not start until the mouse leaves the container being dragged. Otherwise the drag starts immediately upon mouse move, essentially disallowing the drag container to be selected.

There are a bunch of events supported by Dragula that allow you to run code throughout the drag and drop life cycle. The bulk of these events include HTML elements as their arguments, as opposed to Angular variables, unless otherwise stated:

  • drag: This event dispatches when a drag was started.
  • dragend: This event dispatches when a drag event ended, either with a remove, cancel, or drop.
  • drop: This event dispatches when the dragged item was let go.
  • dropModel: This is similar to drop, but the event arguments include an updated model and the item that got dropped.
  • cancel: This event dispatches when the drag is canceled and the dragged item goes back to its container at the original spot.
  • remove: This event dispatches when the dragged item was removed from the container.
  • removeModel: This is similar to remove, but the event arguments include an updated model and the item that got removed.
  • shadow: This event dispatches whenever a dragged item is moved into the container.
  • over: This event dispatches whenever a dragged item is moved over a container.
  • out: This event dispatches when the dragged item is moved out of the container or dropped.
  • cloned: This event dispatches when a dragged item is copied onto a new container, when the copy argument in the Dragula config is set to true.

As you can see the API is fully featured.

I have used drop and dropModel listeners to perform data source object conversions when the objects in the source are different than the object in the destination. I’ve used remove and removeModel to update some data in lists not used for display, but that do care about the underlying data.

Drag and Drop between Containers

Now expand the previous sample to have two containers, and I’ll show you how to drag and drop between them. First, we’ll modify the TypeScript code, then the HTML and CSS.

Open up app.component.ts. Change the ViewChild references from this:

@ViewChild(‘autoscroll’) autoscroll: ElementRef;

To this:

@ViewChild(‘autoscroll1’) autoscroll1: ElementRef;
@ViewChild(‘autoscroll2’) autoscroll2: ElementRef;

We’ll have two containers that support drag and drop, so I added both ViewChildren and named them autoscroll1 and autoscroll2.

We have a characters data source, but now add a selectedCharacters array:

selectedCharacters = [];

This is an empty array for now. The characters array provides all the characters that can be dragged. The selectedCharacters array will contain all the characters that are dropped into the second container.

Look at the ngOnInit() method. The setup of the dragulaService does not change, but the autoScroll will change. Look at this:

autoScroll([
this.autoscroll.nativeElement
], {
margin: 35,
maxSpeed: 4,
scrollWhenOutside: true,
autoScroll() {
return this.down;
}
});

The first argument is an array of elements that the autoScroll library will look at. We want to rename the component from autoscroll to autoscroll1 and add autoscroll2. This should do it:

autoScroll([
this.autoscroll1.nativeElement,
this.autoscroll2.nativeElement
], {
margin: 35,
maxSpeed: 4,
scrollWhenOutside: true,
autoScroll() {
return this.down;
}
});

You can keep the scroll options the same.

I’m going to build the HTML from scratch for you, but you’ll notice a lot of similarities with our previous sample. Start with a page wrapper:

<div class=”wrapper”>
</div>

This is the overall wrapper for our elements, and notice it is no longer an autoscroll container ViewChild. CSS for the wrapper is just a height and width of `100%`.

.wrapper {
width: 100%;
height: 100%;
}

This child is going to need two children, each split between `50%`. This is the first:

<div #autoscroll1 class=”scrollable-wrapper float-left”>
<div class=”dragula_container”
dragula=”DRAGULA_CONTAINER”
[(dragulaModel)]=characters>
<div *ngFor=”let character of characters>
<div class=”character”>
{{character.firstName}} {{character.lastName}}
</div>
</div>
</div>
</div>

The autoscroll1 container ViewChild has two styles associated with it. The first is a scrollable-wrapper:

.scrollable-wrapper {
width:50%;
height: 100%;
text-align: center;
overflow-y: auto;
}

This is what we used for the scrollable container in the previous section, except the width is `50%` instead of `100%`. It also has a float-left CSS class:

.float-left {
float: left
}

This div will be on the left side of the screen, leaving the right side of the screen for our other container.

The next embedded div is the dragula_container. This is where we’re using our previous sample verbatim:

.dragula_container {
height: 100%;
width: 200px;
display: inline-block
}

This just makes sure each character is `200px` wide and the `inline-block` display will stack each character on top of each other, instead of side by side. Finally, add the character CSS block:

.character {
padding: 10px;
background: white;
margin: 5px;
border: grey solid 2px;
width: 100%
}

This puts each character in a box, just like the previous sample.

We’ve looked at the source container, but don’t forget about the destination container:

<div #autoscroll2 class=”scrollable-wrapper”>
<div class=”dragula_container”
dragula=”DRAGULA_CONTAINER”
[(dragulaModel)]=selectedCharacters>
<div *ngFor=”let character of selectedCharacters>
<div class=”character”>
{{character.firstName}} {{character.lastName}}
</div>
</div>
</div>
</div>

The main differences between the first and second containers is that here, the autoscroll div layer does not have the float-left CSS, meaning it will appear to the right of the first div. The dragulaModel is pointed at selectedCharacters instead of characters. And finally, the character loop is also over selectedCharacters instead of characters.

Run your code, and you should see something like this:

Click and drag from the left container to the right a few times. Then go back:

Drag and Copy between Containers

I want to go through one more sample in this article. This section will show you how we can drag from a source container and copy the item into a destination, while also leaving the source container unchanged. I’m going to build off the previous sample to create this one.

First thing we want to do is add IDs to the HTML. Open the app.component.html and find the source container:

<div class=”dragula_container” 
dragula=”DRAGULA_CONTAINER”
[(dragulaModel)]=characters
id=”source”>

Then do the destination:

<div class=”dragula_container” 
dragula=” DRAGULA_CONTAINER”
[(dragulaModel)]=selectedCharacters
id=”destination”>

That is easy enough, now move back to the app.component.ts file. Find the ngOnInit(). We’re going to options to the option array when creating the Dragula group. This is the code:

this.dragulaService.createGroup(‘DRAGULA_CONTAINER’, {});

The second argument is an empty object, but as we discussed earlier, we can add a lot of options into this object. First, add an accepts function:

accepts: (el, target, source, sibling) => {
return target.id !== ‘source’;
},

This says that we only accept the drop if the target.id is something different than `source`. Since this Dragula group is only looking at two containers, it means drop will only work on the destination. Next add a copy function:

copy: (el, source) => {
return source.id === ‘source’;
},

This allows the item to be copied only if the item comes from the source. Both accepts and copy are about performing DOM manipulation as part of the HTML display. But, since we’re dealing with Angular we also have an array behind the scenes, and we’re going to use copyItem to make sure that the underlying data provider array is updated properly:

copyItem: (item) => {
return item;
}

For the purposes of this sample, the copyItem just returns the item. We could create a more complex copyItem function, such as if we had a nested object structure and did not want to share the copy.

Rerun the app, and give it a shot:

You should be able to copy items from the source to the destination. Notice the item is not removed from the source. You will not be able to copy items from the destination to the source. You should be able to rearrange items in the destination, but will not be able to rearrange items inside the source.

Final Thoughts

Since we first researched drag and drop libraries, the Angular Material library introduced Drag and Drop functionality. While this is a compelling offering, we decided not to change course in mid-flight. We explored other options, including using native HTML5 features, but decided that Dragula offered a better experience over other solutions.

--

--

Jeffry Houser
disney-streaming

I’m a technical entrepreneur who likes to build cool things and share them with other people. I’ve been programming applications for a long time.