Building a Draggable Kanban Board in React with Edit and Delete Functionality
Creating a Kanban board, similar to what you might see in Jira, can seem daunting, especially if you try to avoid relying on external libraries. In this tutorial, I'll walk you through how to build a simple, yet effective, draggable Kanban board in React with full support for editing and deleting tasks.
Understanding the Task Structure
The core of our Kanban board revolves around tasks, which are stored as objects with properties like id
, name
, description
, assignee
, and priority
. These tasks will be managed in columns: Backlog, In Progress, and Completed.
The App
Component
The main logic of our Kanban board resides in the App.tsx
file. Here's how to break it down:
State Management:
We use
useState
hooks to manage the state of tasks in each column.newTicket
stores data for creating new tasks.isEditing
,currentTask
, andcurrentColumn
handle editing tasks.isModalOpen
controls the modal visibility for viewing task details.
Adding a New Task:
- The
handleAddTicket
function creates a new task and adds it to the Backlog column.
- The
Editing an Existing Task:
The
handleEditTask
function populates the input fields with the task data, allowing the user to edit it.handleSaveEdit
updates the task in its respective column.
Deleting a Task:
- The
handleDeleteTask
function removes a task from the specified column.
- The
Drag and Drop Functionality:
- We implement
handleDragStart
andhandleDrop
functions to manage the drag-and-drop feature, allowing tasks to move between columns.
- We implement
Task Modal:
Modal.tsx
is used to display task details in a pop-up when clicked.
The Column
Component
The Column.tsx
file manages each column of tasks:
title
: Displays the column title.tasks
: Lists all tasks in the column.onDragStart
andonDrop
: Manage drag-and-drop functionality.onEditTask
,onDeleteTask
, andonViewTask
: Handle task editing, deletion, and viewing, respectively.
The Modal
Component
Finally, the Modal.tsx
file displays the details of a task when it's clicked. This modal can be closed by clicking the "Close" button.
Conclusion
This tutorial guides you through creating a simple, fully functional Kanban board with drag-and-drop, edit, and delete capabilities without using any third-party libraries. The approach is straightforward, making it ideal for beginners and experienced developers looking to build custom project management tools in React.
Feel free to expand upon this basic setup with additional features such as task deadlines, priority sorting, or user-specific task views. Happy coding!
Full Code
App.tsx
import React, { useState } from "react";
import { Column } from "./Column";
import { Modal } from "./Modal";
interface Task {
id: number;
name: string;
description: string;
assignee: string;
priority: string;
}
const App: React.FC = () => {
const [newTicket, setNewTicket] = useState<Partial<Task>>({
name: "",
description: "",
assignee: "",
priority: "",
});
const [completed, setCompleted] = useState<Task[]>([]);
const [inProgress, setInProgress] = useState<Task[]>([]);
const [backlog, setBacklog] = useState<Task[]>([]);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [currentTask, setCurrentTask] = useState<Task | null>(null);
const [currentColumn, setCurrentColumn] = useState<string>("");
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const handleAddTicket = () => {
if (
newTicket.name?.trim() !== "" &&
newTicket.description?.trim() !== "" &&
newTicket.assignee?.trim() !== "" &&
newTicket.priority?.trim() !== ""
) {
const newTask = {
...newTicket,
id: Date.now(),
} as Task;
setBacklog([...backlog, newTask]);
setNewTicket({ name: "", description: "", assignee: "", priority: "" });
}
};
const handleEditTask = (task: Task, column: string) => {
setIsEditing(true);
setCurrentTask(task);
setCurrentColumn(column);
setNewTicket({
name: task.name,
description: task.description,
assignee: task.assignee,
priority: task.priority,
});
};
const handleSaveEdit = () => {
if (
newTicket.name?.trim() === "" ||
newTicket.description?.trim() === "" ||
newTicket.assignee?.trim() === "" ||
newTicket.priority?.trim() === ""
)
return;
const updatedTask = {
...currentTask,
...newTicket,
} as Task;
switch (currentColumn) {
case "Backlog":
setBacklog(
backlog.map((t) => (t.id === updatedTask.id ? updatedTask : t))
);
break;
case "InProgress":
setInProgress(
inProgress.map((t) => (t.id === updatedTask.id ? updatedTask : t))
);
break;
case "Completed":
setCompleted(
completed.map((t) => (t.id === updatedTask.id ? updatedTask : t))
);
break;
default:
break;
}
setIsEditing(false);
setNewTicket({ name: "", description: "", assignee: "", priority: "" });
setCurrentTask(null);
setCurrentColumn("");
};
const handleDeleteTask = (task: Task, column: string) => {
switch (column) {
case "Backlog":
setBacklog(backlog.filter((t) => t.id !== task.id));
break;
case "InProgress":
setInProgress(inProgress.filter((t) => t.id !== task.id));
break;
case "Completed":
setCompleted(completed.filter((t) => t.id !== task.id));
break;
default:
break;
}
};
const handleDragStart = (
e: React.DragEvent<HTMLDivElement>,
task: Task,
sourceColumn: string
) => {
e.dataTransfer.setData("task", JSON.stringify(task));
e.dataTransfer.setData("sourceColumn", sourceColumn);
};
const handleDrop = (
e: React.DragEvent<HTMLDivElement>,
targetColumn: string
) => {
const task = JSON.parse(e.dataTransfer.getData("task")) as Task;
const sourceColumn = e.dataTransfer.getData("sourceColumn");
if (task && sourceColumn && targetColumn !== sourceColumn) {
switch (targetColumn) {
case "Backlog":
setBacklog([...backlog, task]);
break;
case "InProgress":
setInProgress([...inProgress, task]);
break;
case "Completed":
setCompleted([...completed, task]);
break;
default:
break;
}
handleDeleteTask(task, sourceColumn);
}
};
const handleViewTask = (task: Task) => {
setCurrentTask(task);
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
setCurrentTask(null);
};
return (
<>
<div className="flex justify-center items-center p-4">
<input
type="text"
value={newTicket.name}
onChange={(e) => setNewTicket({ ...newTicket, name: e.target.value })}
placeholder="Enter Ticket Name"
className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={newTicket.description}
onChange={(e) =>
setNewTicket({ ...newTicket, description: e.target.value })
}
placeholder="Enter Description"
className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={newTicket.assignee}
onChange={(e) =>
setNewTicket({ ...newTicket, assignee: e.target.value })
}
placeholder="Enter Assignee"
className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="text"
value={newTicket.priority}
onChange={(e) =>
setNewTicket({ ...newTicket, priority: e.target.value })
}
placeholder="Enter Priority"
className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={isEditing ? handleSaveEdit : handleAddTicket}
className={`px-4 py-2 rounded-lg text-white shadow-md ${
isEditing
? "bg-green-500 hover:bg-green-600"
: "bg-blue-500 hover:bg-blue-600"
}`}
>
{isEditing ? "Save Edit" : "Add Ticket"}
</button>
</div>
<div className="flex justify-around p-4 space-x-4">
<Column
title="Backlog"
tasks={backlog}
onDrop={(e) => handleDrop(e, "Backlog")}
onDragStart={handleDragStart}
onEditTask={(task) => handleEditTask(task, "Backlog")}
onDeleteTask={(task) => handleDeleteTask(task, "Backlog")}
onViewTask={handleViewTask}
/>
<Column
title="InProgress"
tasks={inProgress}
onDrop={(e) => handleDrop(e, "InProgress")}
onDragStart={handleDragStart}
onEditTask={(task) => handleEditTask(task, "InProgress")}
onDeleteTask={(task) => handleDeleteTask(task, "InProgress")}
onViewTask={handleViewTask}
/>
<Column
title="Completed"
tasks={completed}
onDrop={(e) => handleDrop(e, "Completed")}
onDragStart={handleDragStart}
onEditTask={(task) => handleEditTask(task, "Completed")}
onDeleteTask={(task) => handleDeleteTask(task, "Completed")}
onViewTask={handleViewTask}
/>
</div>
{isModalOpen && currentTask && (
<Modal currentTask={currentTask} closeModal={closeModal} />
)}
</>
);
};
export default App;
Column.tsx
interface Task {
id: number;
name: string;
description: string;
assignee: string;
priority: string;
}
interface TaskColumnProps {
title: string;
tasks: Task[];
onDrop: (e: React.DragEvent<HTMLDivElement>) => void;
onDragStart: (
e: React.DragEvent<HTMLDivElement>,
task: Task,
sourceColumn: string
) => void;
onEditTask: (task: Task) => void;
onDeleteTask: (task: Task) => void;
onViewTask: (task: Task) => void;
}
export const Column: React.FC<TaskColumnProps> = ({
title,
tasks,
onDrop,
onDragStart,
onEditTask,
onDeleteTask,
onViewTask,
}) => {
const handleTitle = (title: string) => {
return title.length > 15 ? title.slice(0, 15) : title;
};
return (
<div
className="w-1/3 p-4 bg-gray-100 rounded-lg shadow-md"
onDrop={onDrop}
onDragOver={(e) => e.preventDefault()}
>
<h3 className="text-xl font-semibold mb-4 border-b pb-2">{title}</h3>
{tasks.map((task) => (
<div
key={task.id}
className="bg-white p-4 mb-2 rounded-lg shadow cursor-pointer flex justify-between items-center hover:bg-gray-50"
draggable
onDragStart={(e) => onDragStart(e, task, title)}
>
<span onClick={() => onViewTask(task)}>{handleTitle(task.name)}</span>
<div className="flex space-x-2">
<button
onClick={() => onEditTask(task)}
className="text-sm text-blue-500 hover:text-blue-700"
>
Edit
</button>
<button
onClick={() => onDeleteTask(task)}
className="text-sm text-red-500 hover:text-red-700"
>
Delete
</button>
</div>
</div>
))}
</div>
);
};
Modal.tsx
export const Modal = ({ currentTask, closeModal }: any) => {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center">
<div className="bg-white rounded-lg p-8 shadow-lg w-1/2 relative">
<h2 className="text-2xl font-semibold mb-4">Task Details</h2>
<p>
<strong>Name:</strong> {currentTask.name}
</p>
<p>
<strong>Description:</strong> {currentTask.description}
</p>
<p>
<strong>Assignee:</strong> {currentTask.assignee}
</p>
<p>
<strong>Priority:</strong> {currentTask.priority}
</p>
<button
onClick={closeModal}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 absolute top-2 right-2"
>
Close
</button>
</div>
</div>
);
};
Subscribe to my newsletter
Read articles from Harsh Marolia directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by