Building an Angular 4 Drag And Drop Application in 15 Minutes


Last week, I had to build an application that required drag and drop functionality. This made me learn about couple of third party Angular 4 drag-and-drop APIs. The popular options in Angular 2+ world are ng2-dragula and ng2-dnd. I found ng2-dnd more extensible and feature rich so I used it in my application. In today’s blog, we will learn how to add drag-and-drop functionality to a todo application. We will use Addy Osmani Angular 4 todomvc application as the starting point. This post does not cover basics of how to build Angular 4 applications. There are many good references on the web that can teach you building Angular 4 applications from scratch.

Github repository

The code for today’s post in on my Github repository shekhargulati/todomvc-angular-4.

Step 1: Clone the addyosmani/todomvc-angular-4 repository

We will start by cloning addyosmani/todomvc-angular-4 repository on our local machine. To do that you can run the git clone command shown below.

$ git clone git@github.com:addyosmani/todomvc-angular-4.git

Now, change directory to todomvc-angular-4. Install the dependencies and then start the application.

$ cd todomvc-angular-4
$ npm install && npm start

The application will start on port 4200 and will be accessible at http://localhost:4200.

Step 2: Install ng2-dnd

Next, we will install the main dependency of our application ng2-dnd. This library will add support for drag and drop functionality.

$ npm install ng2-dnd --save

Step 3: Import the DndModule

Next, we will import DndModule and declare it in the app.module.ts as shown below. In Angular every application has one application level module that bootstraps the application. The convention is to use AppModule as the name of the module class and app.module.ts as name of the file.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { DndModule } from 'ng2-dnd';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    DndModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

As shown in the code snippet above, we added declaration of DndModule in the imports section. imports defines all the dependencies of a module. As we want to use drag and drag capability provided by ng2-dnd so we declared that in the imports section. The forRoot is a convention for modules that expose a singleton service.

Step 4: Enable drag and drop capability

Now that we have imported DndModule we can add drag-and-drop functionality to our application. Update app.component.html to the one shown below.

<section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
  </header>
  <section class="main" *ngIf="todos.length > 0">
    <ul class="todo-list" dnd-sortable-container [sortableData]="todos">
      <li *ngFor="let todo of todos; let i = index" class="todo-item" [class.completed]="todo.complete" dnd-sortable [sortableIndex]="i">
        <div class="view">

          {{todo.title}}

        </div>
      </li>
    </ul>
  </section>
  <footer class="footer" *ngIf="todos.length > 0">
    <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
  </footer>
</section>

In the code snipped shown above, we did following:

  1. We used dnd-sortable-container directive on ul to mark a container sortable. The reason we are using sortable to make sure items remain in sorted order. The sortableData input is use to specify the array data that will be managed by sortable container.
  2. Next, we used dnd-sortable directive with li to tell that this item can be dragged and sorted. We used sortableIndex to specify position of the item. Items are sorted based on their index in the array.

The code changes made above will enable you to move items around the list by dragging and dropping them. Items will be sorted based on their index. Try it!

Step 5: Add drag-and-drop handle

In the previous step, you could move the item by dragging the list item. These days it is common to have a handle to do it. If you have used Github repository milestone page, you will see that they make use of drag-and-drop handle there. The ng2-dnd library makes it dead simple to add the handle. All you have to do is use dnd-sortable-handle as shown below.

<section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
  </header>
  <section class="main" *ngIf="todos.length > 0">
    <ul class="todo-list" dnd-sortable-container [sortableData]="todos">
      <li *ngFor="let todo of todos; let i = index" class="todo-item" [class.completed]="todo.complete" dnd-sortable [sortableIndex]="i">
        <div class="view">
          <span class="handle">=</span>

          {{todo.title}}

        </div>
      </li>
    </ul>
  </section>
  <footer class="footer" *ngIf="todos.length > 0">
    <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
  </footer>
</section>

Now, you will be able to drag only using the handle.

Step 6: Taking action when item is moved

It is common to take actions when items are moved. ng2-dnd makes it easy to subscribe to such events.

<section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
  </header>
  <section class="main" *ngIf="todos.length > 0">
    <ul class="todo-list" dnd-sortable-container [sortableData]="todos">
      <li *ngFor="let todo of todos; let i = index" class="todo-item" [class.completed]="todo.complete" dnd-sortable [sortableIndex]="i"
      [dragData]="todo" (onDropSuccess)="onMove($event, i)">
        <div class="view">
          <span class="handle">=</span>

          {{todo.title}}

        </div>
      </li>
    </ul>
  </section>
  <footer class="footer" *ngIf="todos.length > 0">
    <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
  </footer>
</section>

In the code snippet shown above, all we did is to use Input to specify what data we want to drag and we specified even handler for onDropSuccess .

Add onMove handler method to app.component.ts.

onMove(todo: Todo, position: number) {
    this.todoDataService.moveTask(todo, position);
}

Component calls todo-data.service.ts. Currently, it only logs the message but it can do interesting stuff like calling the REST API.

moveTask(todo: Todo, position: number): void {
console.log(`Moved ${JSON.stringify(todo)} to the position ${position}`);
}

Step 7: Writing unit test

Let’s now write test case to test our functionality as well.

In the app.component.ts , we will write following test case to test the drag and drop functionality.

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';
import { Todo } from './todo';
import { TodoDataService } from './todo-data.service';
import { By } from '@angular/platform-browser';
import { DndModule } from 'ng2-dnd';

describe('Todolist with drag-and-drop', () => {
  let fixture;
  let app;
  let todoDataService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule,
        DndModule.forRoot()
      ],
      declarations: [
        AppComponent
      ],
      providers: [TodoDataService]
    });
  });

  beforeEach(async(() => {
    fixture = TestBed.createComponent(AppComponent);
    app = fixture.debugElement.componentInstance;
    todoDataService = fixture.debugElement.injector.get(TodoDataService);
  }));

  it('should move todo at top to bottom', (done: any) => {
    addTodo({
      id: 1,
      title: 'First Todo'
    });
    addTodo({
      id: 2,
      title: 'Second Todo'
    });
    addTodo({
      id: 3,
      title: 'Third Todo'
    });
    fixture.detectChanges();
    const todoToDragEl = fixture.debugElement.queryAll(By.css('.todo-item'))[0].nativeElement;
    const todoToDropEl = fixture.debugElement.queryAll(By.css('.todo-item'))[2].nativeElement;
    const handleEl = fixture.debugElement.query(By.css('.handle')).nativeElement;
    triggerEvent(handleEl, 'mousedown', 'MouseEvent');
    triggerEvent(todoToDragEl, 'dragstart', 'MouseEvent');
    triggerEvent(todoToDropEl, 'dragenter', 'MouseEvent');
    triggerEvent(handleEl, 'mouseup', 'MouseEvent');
    triggerEvent(todoToDragEl, 'drop', 'MouseEvent');
    fixture.detectChanges();
    expect(app.todos.map(t => t.id)).toEqual([2, 3, 1]);
    done();
  });

  function addTodo(obj) {
    app.newTodo = new Todo(obj);
    app.addTodo();
  }

  function triggerEvent(elem: HTMLElement, eventName: string, eventType: string) {
    const event: Event = document.createEvent(eventType);
    event.initEvent(eventName, true, true);
    elem.dispatchEvent(event);
  }
});

You can run the test case by running npm test command.

Conclusion

In this post, we looked at how easy it is to add drag-and-drop capability to the application by using ng2-dnd library. Try it out.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s