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
That’s how a drag and drop board should look
To archive the drag and drop functionality, I head straight to @angular/cdk/drag-drop
and import its DragDropModule
The @angular/cdk/drag-drop
module provides you with a way to easily and declaratively create drag-and-drop interfaces, with support for:
Based on the features that cdk/drag-drop
provided, I can build the board drag and drop easily 🤣
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.
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
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;
}
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>
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.
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.
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;
}
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.
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.
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);
}
}
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.
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!
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!