How To Build an App With Drag and Drop With Angular
Tags: javascript, angular, typescript, tutorial
Published: 2020-01-12 00:06:00

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

Drag and drop is a feature of many interactive web apps. It provides an intuitive way for users to manipulate their data. Adding a drag and drop feature is easy for Angular apps.

In this piece, we will create a to-do app that has two columns: a _to-do_ column and a _done_ column. You can drag and drop between the two to change the status from to-do to done and vice versa.

To build the app, we use the [Angular Material](https://material.angular.io/) library to make the app look good and to easily provide drag and drop abilities to our app. It will also have a navigation menu and a top bar.

To start building the app, we install the Angular CLI by running `npm i @angular/cli`. We should choose to include routing and use SCSS when asked.

Then, we create a new Angular project by running `ng new todo-app`. After that, we add the libraries we need, by running `npm i@angular/cdk @angular/material @ngrx/store`.

This will add Angular Material and the NGRX store to our app. We will use [Flux](https://facebook.github.io/flux/) extensively with this app. Next, we add our components and services by running the following:

```bash
ng g component addTodoDialog  
ng g component homePage  
ng g component toolBar  
ng g service todo
```

We add the boilerplate for the NGRX store by running `ng add @ngrx/store`.

Now, we can build the logic for our app. In `add-todo-dialog.component.ts`, we add:

```typescript
import { Component, OnInit } from '@angular/core';  
import { NgForm } from '@angular/forms';  
import { MatDialogRef } from '@angular/material/dialog';  
import { Store } from '@ngrx/store';  
import { TodoService } from '../todo.service';  
import { SET_TODOS } from '../reducers/todo-reducer';

@Component({  
  selector: 'app-add-todo-dialog',  
  templateUrl: './add-todo-dialog.component.html',  
  styleUrls: ['./add-todo-dialog.component.scss']  
})  
export class AddTodoDialogComponent implements OnInit {  
  todoData: any = <any>{  
    done: false  
  }; 

  constructor(  
    public dialogRef: MatDialogRef<AddTodoDialogComponent>,  
    private todoService: TodoService,  
    private store: Store<any>  
  ) { } 

  ngOnInit() {  
  } 

  save(todoForm: NgForm) {  
    if (todoForm.invalid) {  
      return;  
    }  
    this.todoService.addTodo(this.todoData)  
      .subscribe(res => {  
        this.getTodos();  
        this.dialogRef.close();  
      })  
  } 

  getTodos() {  
    this.todoService.getTodos()  
      .subscribe(res => {  
        this.store.dispatch({ type: SET_TODOS, payload: res });  
      })  
  }  
}
```

This code is for the dialog box that lets us add the to-do items to a list, then get the latest items and put them in the store.

In `add-todo-dialog.component.html`, we add:

```html
<h2>Add Todo</h2>  
<form #todoForm='ngForm' (ngSubmit)='save(todoForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Description" required #description='ngModel' name='description'  
            [(ngModel)]='todoData.description'>  
        <mat-error *ngIf="description.invalid && (description.dirty || description.touched)">  
            <div *ngIf="content.errors.required">  
                Description is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Add</button>  
</form>
```

This is the form for adding the to-do item. It only has one field, the description, to keep it simple.

In `add-todo-dialog.component.scss`, to change the width of the form field, we put:

```scss
form {  
  mat-form-field {  
    width: 100%;  
    margin: 0 auto;  
  }  
}
```

Next, we build our home page. This is where the two lists will reside. The user can drag and drop between the two lists to change the status of the task.

In `home-page.component.ts`, we put:

```typescript
import { Component, OnInit } from '[@angular/core](http://twitter.com/angular/core)';  
import { MatDialog } from '[@angular/material](http://twitter.com/angular/material)/dialog';  
import { AddTodoDialogComponent } from '../add-todo-dialog/add-todo-dialog.component';  
import { TodoService } from '../todo.service';  
import { Store, select } from [@ngrx/store';  
import { SET_TODOS } from '../reducers/todo-reducer';  
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';

@Component({  
  selector: 'app-home-page',  
  templateUrl: './home-page.component.html',  
  styleUrls: ['./home-page.component.scss']  
})  
export class HomePageComponent implements OnInit {  
  allTasks: any[] = [];  
  todo: any[] = [];  
  done: any[] = []; 

  constructor(  
    public dialog: MatDialog,  
    private todoService: TodoService,  
    private store: Store<any>  
  ) {  
    store.pipe(select('todos'))  
      .subscribe(allTasks => {  
        this.allTasks = allTasks || [];  
        this.todo = this.allTasks.filter(t => !t.done);  
        this.done = this.allTasks.filter(t => t.done);  
      })  
  } 

  ngOnInit() {  
    this.getTodos();  
  } 

  openAddTodoDialog() {  
    const dialogRef = this.dialog.open(AddTodoDialogComponent, {  
      width: '70vw',  
      data: {}  
    }) dialogRef.afterClosed().subscribe(result => {  
      console.log('The dialog was closed');  
    });  
  } 

  getTodos() {  
    this.todoService.getTodos()  
      .subscribe(res => {  
        this.store.dispatch({ type: SET_TODOS, payload: res });  
      })  
  } 

  drop(event: CdkDragDrop<any[]>) {  
    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);  
    }  
    let data = event.container.data[0];  
    data.done = !data.done;  
    this.todoService.editTodo(data)  
      .subscribe(res => {})  
  } 

  removeTodo(index: number, tasks: any[]) {  
    const todoId = tasks[index].id;  
    this.todoService.removeTodo(todoId)  
      .subscribe(res => {  
        this.getTodos();  
      })  
  }  
}
```

We have functions to handle the dropping of the to-do items and we let users open the _add to-do_ dialog that we built earlier with the `openAddTodoDialog` function.

User can also remove to-do’s on this page with the `removeTodo` function. The `drop` function handles the dropping between lists and also toggles the status of the to-do item.

The order of this is important. The `if...else` block should come before calling the `editTodo` function because the item will not be in the `event.container.data` array otherwise.

The `removeTodo` takes both an `index` and the list of `tasks` because it is used for both the `todo` and `done` arrays.

In `home-page.component.html`, we add:

```html
<div class="center">  
    <h1>Todos</h1>  
    <button mat-raised-button (click)='openAddTodoDialog()'>Add Todo</button>  
</div>  
<div class="content">  
    <div class="todo-container">  
        <h2>To Do</h2><div cdkDropList #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList]"  
            class="todo-list" (cdkDropListDropped)="drop($event)">  
            <div class="todo-box" *ngFor="let item of todo; let i = index" cdkDrag>  
                {{item.description}}  
                <a class="delete-button" (click)='removeTodo(i, todo)'>  
                    <i class="material-icons">  
                        close  
                    </i>  
                </a>  
            </div>  
        </div>  
    </div><div class="done-container">  
        <h2>Done</h2><div cdkDropList #doneList="cdkDropList" [cdkDropListData]="done" [cdkDropListConnectedTo]="[todoList]"  
            class="todo-list" (cdkDropListDropped)="drop($event)">  
            <div class="todo-box" *ngFor="let item of done; let i = index" cdkDrag>  
                {{item.description}}  
                <a class="delete-button" (click)='removeTodo(i, done)'>  
                    <i class="material-icons">  
                        close  
                    </i>  
                </a>  
            </div>  
        </div>  
    </div>
</div>
```

This template provides the two lists for the user to drag and drop between to toggle the status. There is also an ‘x’ button is each list item to let users delete the item.

In `home-page.component.scss`, we add:

```scss
$gray: gray;.content {  
  display: flex;  
  align-items: flex-start;  
  margin-left: 2vw;  
  div {  
    width: 45vw;  
  }  
}

.todo-container {  
  width: 400px;  
  max-width: 100%;  
  margin: 0 25px 25px 0;  
  display: inline-block;  
  vertical-align: top;  
}

.todo-list {  
  border: solid 1px $gray;  
  min-height: 70px;  
  background: white;  
  border-radius: 4px;  
  overflow: hidden;  
  display: block;  
}

.todo-box {  
  padding: 20px 10px;  
  border-bottom: solid 1px $gray;  
  color: rgba(0, 0, 0, 3);  
  display: flex;  
  flex-direction: row;  
  align-items: center;  
  justify-content: space-between;  
  box-sizing: border-box;  
  cursor: move;  
  background: white;  
  font-size: 14px;  
  height: 70px;  
}

.cdk-drag-preview {  
  box-sizing: border-box;  
  border-radius: 4px;  
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 2), 0 8px 10px 1px rgba(0, 0, 0, 1), 0 3px 14px 2px rgba(0, 0, 0, 6);  
}

.cdk-drag-placeholder {  
  opacity: 0;  
}

.cdk-drag-animating {  
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);  
}

.todo-box:last-child {  
  border: none;  
}

.todo-list.cdk-drop-list-dragging .todo-box:not(.cdk-drag-placeholder) {  
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);  
}

.delete-button {  
  cursor: pointer;  
}
```

To style the lists and the boxes so they have a border and a shadow.

We move on to adding the reducers for our store. We create a file, called `menu-reducer.ts`, by running `ng g class menuReducer`.

In there, we add:

```typescript
export const SET_MENU_STATE = 'SET_MENU_STATE';

export function MenuReducer(state: boolean, action) {  
    switch (action.type) {  
        case SET_MENU_STATE:  
            return action.payload; 
        default:  
            return state;  
    }  
}
```

Similarly, we create a file, called `todo-reducer.ts`, by running `ng g class todoReducer`, and add:

```typescript
const SET_TODOS = 'SET_TODOS';

function todoReducer(state, action) {  
    switch (action.type) {  
        case SET_TODOS:  
            state = action.payload;  
            return state;  
        default:  
            return state  
    }  
}

export { todoReducer, SET_TODOS };
```

In `reducers/index.ts`, we put:

```typescript
import { MenuReducer } from './menu-reducer';  
import { todoReducer } from './todo-reducer';
export const reducers = {  
  menuState: MenuReducer,  
  todos: todoReducer  
};
```

So that the reducers can be passed into the `StoreModule` when we import it in `app.module.ts`. The three files together will create the store that we use to store all the to-do tasks.

Next, in `tool-bar.component.ts`, we put:

```typescript
import { Component, OnInit } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { SET_MENU_STATE } from '../reducers/menu-reducer';

@Component({  
  selector: 'app-tool-bar',  
  templateUrl: './tool-bar.component.html',  
  styleUrls: ['./tool-bar.component.scss']  
})  
export class ToolBarComponent implements OnInit {  
  menuOpen: boolean; constructor(  
    private store: Store<any>  
  ) {  
    store.pipe(select('menuState'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  ngOnInit() {  
  }

  toggleMenu() {  
    this.store.dispatch({ type: SET_MENU_STATE, payload: !this.menuOpen });  
  }
}
```

To let users toggle the left menu on and off. Then, in `tool-bar.component.html`, we put:

```html
<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    Todo App  
</mat-toolbar>
```

To add the top toolbar and the menu.

In `tool-bar.component.scss`, we add:

```scss
.menu-button {  
  margin-top: 6px;  
  margin-right: 10px;  
  cursor: pointer;  
}

.mat-toolbar {  
  background: #009688;  
  color: white;  
}
```

To add some spacing to our menu button and title text.

In `app-routing.module.ts`, we replace the existing code with:

```typescript
import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';

const routes: Routes = [  
  { path: '', component: HomePageComponent },  
];

@NgModule({  
  imports: [RouterModule.forRoot(routes\],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }
```

To let users see the home page.

Then, in `app.component.ts`, we put:

```typescript
import { Component, HostListener } from '@angular/core';  
import { SET_MENU_STATE } from './reducers/menu-reducer';  
import { Store, select } from '@ngrx/store';

@Component({  
  selector: 'app-root',  
  templateUrl: './app.component.html',  
  styleUrls: ['./app.component.scss']  
})  
export class AppComponent {  
  menuOpen: boolean; constructor(  
    private store: Store<any>,  
  ) {  
    store.pipe(select('menuState'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  } 

  @HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: SET_MENU_STATE, payload: this.menuOpen });  
    }  
  }
}
```

To toggle the menu off when the user clicks outside of the menu button and menu. In `app.component.html`, we add:

```html
<mat-sidenav-container class="example-container">  
  <mat-sidenav mode="side" [opened]='menuOpen'>  
      <ul>  
          <li>  
              <b>  
                  New York Times  
              </b>  
          </li>  
          <li>  
              <a routerLink='/'>Home</a>  
          </li>  
      </ul>
  </mat-sidenav>  
  <mat-sidenav-content>  
      <app-tool-bar></app-tool-bar>  
      <div id='content'>  
          <router-outlet></router-outlet>  
      </div>  
  </mat-sidenav-content>  
</mat-sidenav-container>
```

To add the menu, the left-side navigation, and `router-outlet` element to let people see the routes we defined.

In `app.component.scss`, we add:

```scss
#content {  
  padding: 20px;  
  min-height: 100vh;  
}

ul {  
  list-style-type: none;  
  margin: 0;  
  li {  
    padding: 20px 5px;  
  }  
}
```

To add some padding to the pages and change the list style of the items in the left side menu.

In `environment.ts`, we add:

```typescript
export const environment = {  
  production: false,  
  apiUrl: 'http://localhost:3000'
};
```

To add the URL for our API.

In `styles.scss`, we add:

```scss
/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";

body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}
```

To import the [Material Design](https://material.io/design/) theme and change the form field’s widths.

In `app.module.ts`, we replace the existing code with:

```typescript
import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
import { FormsModule } from '@angular/forms';  
import { AppRoutingModule } from './app-routing.module';  
import { AppComponent } from './app.component';  
import { HomePageComponent } from './home-page/home-page.component';  
import { StoreModule } from '@ngrx/store';  
import { reducers } from './reducers';  
import { MatSidenavModule } from '@angular/material/sidenav';  
import { MatToolbarModule } from '@angular/material/toolbar';  
import { MatInputModule } from '@angular/material/input';  
import { MatFormFieldModule } from '@angular/material/form-field';  
import { ToolBarComponent } from './tool-bar/tool-bar.component';  
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
import { MatButtonModule } from '@angular/material/button';  
import { HttpClientModule } from '@angular/common/http';  
import { MatSelectModule } from '@angular/material/select';  
import { MatCardModule } from '@angular/material/card';  
import { MatListModule } from '@angular/material/list';  
import { MatMenuModule } from '@angular/material/menu';  
import { MatIconModule } from '@angular/material/icon';  
import { MatGridListModule } from '@angular/material/grid-list';  
import { AddTodoDialogComponent } from './add-todo-dialog/add-todo-dialog.component';  
import { DragDropModule } from '@angular/cdk/drag-drop';

@NgModule({  
  declarations: [  
    AppComponent,  
    HomePageComponent,  
    ToolBarComponent,  
    AddTodoDialogComponent  
  ],  
  imports: [  
    BrowserModule,  
    AppRoutingModule,  
    StoreModule.forRoot(reducers),  
    FormsModule,  
    MatSidenavModule,  
    MatToolbarModule,  
    MatInputModule,  
    MatFormFieldModule,  
    BrowserAnimationsModule,  
    MatButtonModule,  
    MatMomentDateModule,  
    HttpClientModule,  
    MatSelectModule,  
    MatCardModule,  
    MatListModule,  
    MatMenuModule,  
    MatIconModule,  
    MatGridListModule,  
    DragDropModule  
  ],  
  providers: [],  
  bootstrap: [AppComponent],  
  entryComponents: [  
    AddTodoDialogComponent  
  ]  
})  
export class AppModule { }
}
```

In `todo.service.ts`, we put:

```typescript
import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { environment } from 'src/environments/environment';

@Injectable({  
  providedIn: 'root'  
})  
export class TodoService { 

  constructor(  
    private http: HttpClient  
  ) { } 

  getTodos() {  
    return this.http.get(`${environment.apiUrl}/todos`);  
  } 

  addTodo(data) {  
    return this.http.post(`${environment.apiUrl}/todos`, data);  
  } 

  editTodo(data) {  
    return this.http.put(`${environment.apiUrl}/todos/${data.id}`, data);  
  } 

  removeTodo(id) {  
    return this.http.delete(`${environment.apiUrl}/todos/${id}`);  
  }  
}
```

These functions let us do CRUD operations for our to-do items by making requests to our JSON API, which we will be added using the [JSON Server](https://github.com/typicode/json-server) Node.js package.

Data will be saved to a JSON file, so we don’t have to make our own back end add it, to save some simple data. We install the server by running `npm i -g json-server`.

Once that is done, go into the project directory and run `json-server --watch db.json`. In `db.json`, we put:

```json
{  
  "todos": []  
}
```

So that we can use those endpoints for saving data to `db.json`.