Angular — An extremely simple and easy implementation of drag and drop list reordering without library

Drag and drop list reordering is a very common feature of webpages with lists of data, such as Trello, Jira, etc. It is also a frequently asked question during frontend developer interviews. It was once one of the most difficult features to implement due to huge amount of DOM manipulations and overlap calculations. Yet, with the help of the HTML drag and drop API and modern frontend frameworks, this feature now only needs tens of lines to implement.

Here I am going to go through an example on implementing the drag and drop reordering feature in an exiting page with a list of data.

Implementation

Say we have a list of data in our page:

@Component({
...
template: `
<div class="card" *ngFor="let row of data">
{{ row }}
</div>
`,
styles: [`
.card {
border: 1px solid black;
padding: 4px;
margin: 8px 0;
}
`]
})
export class ListComponent {
data: string[] = [...];
}

The first step is to connect the drag and drop API with the component.

@Component({
...
template: `
<div class="drag-wrapper"
*ngFor="let row of data; let index = index"
[draggable]="true" (dragstart)="onDragStart(index)"
(dragenter)="onDragEnter(index)" (dragend)="onDragEnd()">
<div class="card">{{ row }}</div>
</div>
`,
styles: [`
.drag-wrapper {
padding: 4px 0;
}

.card {
border: 1px solid black;
padding: 4px;
}
`]
})
export class ListComponent {
data: string[] = [...];
...
}

First, we enable the drag and drop API on the given items via binding true to [draggable]. For the three events, dragstart and dragend are pretty self-descriptive which fire when the drag starts and ends, while dragenter refers to the moment when the bound items collide with the item that is currently being dragged. Besides, the reason to wrap the items with wrappers is to get rid of the margin, which make the detection of collision unsmooth.

@Component(...)
export class ListComponent {
...
draggingIndex: number;
onDragStart(fromIndex: number): void {
this.draggingIndex = fromIndex;
}

onDragEnter(toIndex: number): void {
if (this.draggingIndex !== toIndex) {
this._reorderItem(this.draggingIndex, toIndex);
}
}

onDragEnd(): void {
this.draggingIndex = undefined;
}
}

The core logic is quite straight forward. When a item is dragged, we record the item index with a local state, which is reset when the dragging stops. When two items collide, we execute the function to reorder those items.

private _reorderItem(fromIndex: number, toIndex: number): void {
const itemToBeReordered = this.data.splice(fromIndex, 1)[0];
this.data.splice(toIndex, 0, itemToBeReordered);
this.draggingIndex = toIndex;
}

The ordering logic depends on the data structures and UI layout. Here is an implementation for array data structure and layout with only one column.

Finally, we can further improve the UX by adding UI feedback when users drag the item [class.dragging]=”index === this.draggingIndex”.

Runnable example

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

@Component({
selector: 'app-list',
template: `
<div class="drag-wrapper"
*ngFor="let row of data; let index = index"
[draggable]="true"
[class.dragging]="index === this.draggingIndex"
(dragstart)="onDragStart(index)"
(dragenter)="onDragEnter(index)"
(dragend)="onDragEnd()">
<div class="card">
{{ row }}
</div>
</div>
`,
styles: [`
.drag-wrapper {
padding: 4px 0;
}

.drag-wrapper.dragging {
opacity: 0.5;
}

.card {
border: 1px solid black;
padding: 4px;
}
`]
})
export class ListComponent {
data: string[] = [
'Melvin Walter Kissling Gam (April 25, 1931 – January 28, 2002) was a Costa Rican businessman who became',
'chool campus in Costa Rica, as well as founding the Costa Rican non-profit organization Asociación de Empresarios para el',
'Walter Kissling was born to Walter Kissling Rickli and Adela Gam Secen in Limón, Costa Rica on April 25, 1931',
'r interview in 1998 he mentioned his mother as driving force in his life. “She was a fighting woman.',
'Kissling graduated from Colegio Seminario in San José in 1948. After graduation he worked selling cheese and as a receptionist in',
'rican businessman who gave him 2 pamphlets and told him that if he learned them by heart he would hire him as',
'Seeking to advance his career, Kissling joined Kativo Chemical in 1953. At that time the company was a small business',
'l American markets.[1] With the acquisition of Kativo Chemical by H.B. Fuller in 1967, Kissling',
'ile maintaining his role as general manager at Kativo Chemical. He then led the opening of several H.B Fuller',
'He went on to serve the company in significant leadership positions, including senior vice president of international operations, and executive'
];
draggingIndex: number;

private _reorderItem(fromIndex: number, toIndex: number): void {
const itemToBeReordered = this.data.splice(fromIndex, 1)[0];
this.data.splice(toIndex, 0, itemToBeReordered);
this.draggingIndex = toIndex;
}

onDragStart(index: number): void {
this.draggingIndex = index;
}

onDragEnter(index: number): void {
if (this.draggingIndex !== index) {
this._reorderItem(this.draggingIndex, index);
}
}

onDragEnd(): void {
this.draggingIndex = undefined;
}
}

Thanks for reading! Hope you find this article helpful. Any comments would be highly appreciated. :D

Web developer from Hong Kong. Most interested in Angular and Vue. Currently working on a Nuxt.js + NestJS project.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store