Wanna see something cool? Check out Angular Spotify 🎧

Angular Jira Clone Part 05 - Build an interactive drag and drop board

My fifth tutorial for jira.trungk18.com will focus on one of the most interesting features - drag and drop board.

See all tutorials for Jira clone

Requirement

That’s how a drag and drop board should look

Angular Jira Clone Part 05 - Build an interactive drag and drop board

  1. There are several columns/lanes, each lane will have a list of card/issue
  2. A card can be moved within the same column
  3. A card can also be moved between different column

To archive the drag and drop functionality, I head straight to @angular/cdk/drag-drop and import its DragDropModule

Source code and demo

  1. https://stackblitz.com/edit/angular-board-drag-and-drop
  2. board-dnd-list.component.ts
  3. https://jira.trungk18.com/

Drag and drop

The @angular/cdk/drag-drop module provides you with a way to easily and declaratively create drag-and-drop interfaces, with support for:

  • Free dragging
  • Sorting within a list
  • Transferring items between lists
  • Animations
  • Touch devices
  • Custom drag handles, previews, and placeholders
  • Horizontal lists and locking along an axis

Based on the features that cdk/drag-drop provided, I can build the board drag and drop easily 🤣

To make an element draggable

Start by importing DragDropModule into the NgModule where you want to use drag-and-drop features. You can now add the cdkDrag directive to elements to make them draggable.

This is the HTML code with cdkDrag attached.

<div class="example-box" cdkDrag>
  Simple div - Drag me around
</div>

You need some simple CSS too. Usually, you will need to set the transition property.

.example-box {
  //code removed for brevity
  z-index: 1;
  transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
  box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
    0 1px 5px 0 rgba(0, 0, 0, 0.12);
}

.example-box:active {
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14),
    0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

And voila, that’s the result.

Angular Jira Clone Part 05 - Build an interactive drag and drop board

Prepare the layout for drag and drop between multiple columns

You can add cdkDropList elements to constrain where elements may be dropped. When outside of a cdkDropList element, draggable elements can be freely moved around the page as the above example.

First, let look into my data model how I represent the lane view. For each lane, there will be a list of issues. For example “Backlog” lane has two issues, “In progress” lane has three issues.

export class JLane {
  id: IssueStatus
  title: string
  issues: JIssue[]
}

export interface JIssue {
  id: string
  title: string
  status: IssueStatus
  type: IssueType
  priority: IssuePriority
}

For the sake of this blog post simplicity, each issue will only contain some basic information. The most important property of an issue is the id and status. You might notice the issue status is for grouping them into a different lane.

I have this structure of components in mind

  • BoardDndComponent: the parent component that take a list of lane and render them

    • BoardDnDListComponent: to render a lane with a list of issue
    • IssueCardComponent: to represent a single issue

1. IssueCardComponent

This component is simple, only take an issue which is in type JIssue and render it to the UI. For now, only the issue title will be displayed.

export class IssueCardComponent implements OnInit {
  @Input() issue: JIssue
}
<div class="issue-wrap">
  <div class="issue">
    <p class="pb-3 text-15 text-textDarkest">
      {{ issue.title }}
    </p>
  </div>
</div>
.issue-wrap {
  touch-action: manipulation;
  cursor: -webkit-grab;
  cursor: grab;
  margin-bottom: 5px;
}

.issue {
  display: flex;
  flex-grow: 1;
  flex-direction: column;
  border-radius: 0.125rem;
  background-color: #fff;
  transition-property: all;
  transition-duration: 0.1s;
  padding: 10px;
}

2. BoardDnDListComponent

This component will take a lane as input and render a list of issues for that lane. Also, we will be using cdkDropList and cdkDrag on that component to enable the drag and drop capability.

Take note that I set the selector surrounded by a square bracket [board-dnd-list], which mean I will do the attribute selection for that component, to reduce one level deeper of CSS styling purpose, you will see on the BoardDndComponent

@Component({
  selector: '[board-dnd-list]',
  templateUrl: './board-dnd-list.component.html',
  styleUrls: ['./board-dnd-list.component.css'],
})
export class BoardDndListComponent implements OnInit {
  @Input() lane: JLane
}

I also associated some arbitrary data with both cdkDrag and cdkDropList by setting cdkDragData and cdkDropListData to the issue and the list of issue, respectively. Events fired from both directives include this data, allowing to easily identify the origin of the drag or drop interaction. The lane id will also be set to the cdkDropList.

<div class="status-list">
  <div class="px-3 pb-4 pt-3">
    {{ lane.title }}
  </div>
  <div
    class="issue-card-container pl-2"
    cdkDropList
    [cdkDropListData]="lane.issues"
    [id]="lane.id"
  >
    <issue-card
      *ngFor="let issue of lane.issues"
      [issue]="issue"
      [cdkDragData]="issue"
      cdkDrag
    >
    </issue-card>
  </div>
</div>

3. BoardDndComponent

That final part is to glue them together. Because I have an unknown number of connected drop lists, I set the cdkDropListGroup directive to set up the connection automatically. Note that any new cdkDropList that is added under a group will be connected to all other lists automatically.

<div class="d-flex" cdkDropListGroup>
  <div
    class="board-dnd-list"
    board-dnd-list
    *ngFor="let lane of lanes"
    [lane]="lane"
  ></div>
</div>

And that’s the result.

Angular Jira Clone Part 05 - Build an interactive drag and drop board

As you can see, I can start dragging the card. But still, need to handle the animation and after the drop event and update the data to get displayed on the UI.

Adding animation

Follow styling section, I will modify some of the class that was added by the directives.

First, I need to style IssueCardComponent component host style to make it has a property height.

:host {
  display: flex;
  flex: 1;
}

cdk-drag-placeholder class

This is an element that will be shown instead of the real element as it’s being dragged inside a cdkDropList. By default, this will look exactly like the element that is being sorted.

I need to style this class to make my element look different where it is staying while being dragged around.

This is the current behavior before styling.

Angular Jira Clone Part 05 - Build an interactive drag and drop board

I wanted the current card to look like a place holder only with a dashed border, and the content is invisible.

.cdk-drag-placeholder {
  .issue-wrap {
    background-color: rgba(150, 150, 200, 0.1);
    border: 1px dashed #abc;
    margin: 5px;

    .issue {
      opacity: 0;
    }
  }
}

And that’s how it looks after styling.

Angular Jira Clone Part 05 - Build an interactive drag and drop board

cdk-drop-list-dragging class

A class that is added to cdkDropList while the user is dragging an item.

I style that to have some animation in place.

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

Angular Jira Clone Part 05 - Build an interactive drag and drop board

Update data after dropping

So what you have seen so far is just the UI, after drag and drop and item. You need to update the data to reflect what has been changed on the UI, whether it is updating the position property or just an index of an array.

You can listen to the cdkDropListDropped event on where you attached cdkDropList directive to handle.

<div
  class="issue-card-container pl-2"
  cdkDropList
  [cdkDropListData]="lane.issues"
  (cdkDropListDropped)="drop($event)"
  [id]="lane.id"
></div>

The function is pretty simple. If it is happening inside the same lane, you call the build-in util function of cdk to moveItemInArray. If it is moving between two lanes, call a function to update the indices between array.

Noted that those utils will modify the array in place. So if you are using any state management, you should consider copying the array to the new one before modifying.

drop(event: CdkDragDrop<JIssue[]>) {
  let isMovingInsideTheSameList = event.previousContainer === event.container;
  if (isMovingInsideTheSameList) {
    moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
  }
  else {
    transferArrayItem(
      event.previousContainer.data,
      event.container.data,
      event.previousIndex,
      event.currentIndex
    )
  }
}

See the result, looks good now. But do you notice something? Seems like you can’t drag the card to the end of the other list.

Angular Jira Clone Part 05 - Build an interactive drag and drop board

To fix that, simply set the container for the list of issue-card to be full height.

.status-list {
  //code removed for brevity

  .issue-card-container {
    height: 100%;
  }
}

The final result is here!

Angular Jira Clone Part 05 - Build an interactive drag and drop board

That’s all for the fifth part. Any questions, you can leave it on the comment box below or reach me on Twitter. Thanks for stopping by!

Published 13 Sep 2020

Read more

 — How to kill the process currently using a given port on Windows
 — Use VSCode Like a PRO
 — Angular Jira Clone Part 04 - Build an editable textbox
 — Angular Jira Clone Part 03 - Setup Akita state management
 — Angular Jira Clone Part 02 - Build the application layout with flex and TailwindCSS

Follow @trungvose on Twitter for more!