Mega Tutorial: How to build a Trello code in Django (Part 6)
Home Django Mega Tutorial: Build a Trello Clone - Part 6

Mega Tutorial: Build a Trello Clone - Part 6

04 April, 2025

In this episode from our "Build a Trello Clone" series, we will be handling the dragging and dropping tasks between columns. For me, that was the coolest feature of Trello when I first started using it!

Creating Alpine.js Board Application

Dragging and Dropping might seem complex, but Alpine.js makes it easy for us. Let's first write the application that will be assigned to the container of the columns (this can be included in the content block inside the boards/templates/boards/board_detail.html file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<script>
     BoardApp = function() {
         return {
             draggedTask: null,

             startDrag(event, taskId, listId) {
                 this.draggedTask = taskId;
             },

             endDrag() {
                 this.draggedTask = null;
             },
             moveTask(event, targetListId) {
                 if (this.draggedTask === null || this.draggedFromList === targetListId) {
                     return;
                 }

                 taskId = this.draggedTask;

                 // Reset dragged state
                 this.draggedTask = null;

                 const data = new URLSearchParams();
                 data.append("task_id", taskId);
                 data.append("column_id", targetListId);
                 data.append("csrfmiddlewaretoken", "{{ csrf_token }}");
                 data.append("action", "move_card");

                 // Send update to backend
                 fetch('{% url 'board_detail' pk=object.pk %}', {
                     method: 'POST',
                     body: data,
                     redirect: 'manual'
                 }).then(response => {
                     // Refresh page
                     window.location.href = '{% url 'board_detail' pk=object.pk %}'
                 })
             }
         }
     }
</script>

Drag/Drop Events

We will be using the following Alpine.js events:

  • @dragstart - Event triggered on the Task card when we start dragging
  • @dragend - Event triggered on the Task card when we stop dragging
  • @drop - Event triggered on the Column when a card is dropped

Initializing BoardApp

We now need to modify a bit the container <div> from:

1
2
3
{% block content %}
     <div class="overflow-x-scroll">
         <div class="flex min-w-max gap-4">

to:

1
2
3
{% block content %}
    <div class="overflow-x-scroll" x-data="BoardApp()">
         <div class="flex min-w-max gap-4 items-start">

Attaching the event handlers for tasks

Let's go to boards/templates/boards/partial/task_card.html and add these event handlers to the Task card:

1
2
3
4
5
6
7
8
    <div class="bg-gray-200 rounded p-4 border border-black flex flex-col justify-between"
         draggable="true"
         @dragstart="startDrag(event, {{ card.pk }}, {{ card.column.pk }})"
         @dragend="endDrag()">
            <div>
                <a href="{% url 'card_detail' pk=card.pk board_id=card.column.board.pk %}">
                    {{ object.title }}
                    ...

Attach event handlers on columns

Let's head back to boards/templates/boards/board_detail.html and attach these events to the columns so that we can drop tasks in them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{% for column in object.columns.all %}
     <div class="bg-white rounded shadow p-4 w-72 border-2 border-black">
         <h2 class="font-bold text-lg mb-4">{{ column.title }}</h2>
         <div class="space-y-4">
         <div class="space-y-4 min-h-4"
              @dragover.prevent
              @drop="moveTask(event, {{ column.pk }})">
             {% for card in column.cards.all %}
                 {% include "boards/partial/task_card.html" with object=card %}
             {% endfor %}

Write the backend code

Notice how in the moveTask function we posted the task_id and the column_id, while specifying that the action is move_task. There's no actual code that handles this action. Let's write it!

We simply need to add the move_card handler inside the post method of the BoardDetailView view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@method_decorator(login_required(login_url=reverse_lazy('login')), name='dispatch', )
class BoardDetailView(DetailView):
     template_name = "boards/board_detail.html"
     model = Board

     def post(self, request, *args, **kwargs):
         self.object = self.get_object()
         if request.POST.get("action") == "create_column":
             title = request.POST.get("title")
             if title:
                 Column.objects.create(
                     board=self.object,
                     title=title
                 )

         if request.POST.get("action") == "create_card":
             title = request.POST.get("title")
             column = get_object_or_404(Column, pk=int(request.POST.get("column_id")))
             if title:
                 Card.objects.create(title=title, column=column)

         if request.POST.get("action") == "move_card":   # Handle Drag and Drop
             if request.POST.get("column_id", None) is not None:
                 task = get_object_or_404(Card, pk=int(request.POST.get("task_id")))
                 column = get_object_or_404(Column, pk=int(request.POST.get("column_id")))
                 task.column = column
                 task.save()

         return redirect("board_detail", pk=self.object.pk)

Final Result

Here's how dragging and dropping tasks around looks like:

Drag and Drop cards
Drag and Drop cards

See you soon!