TypeScript Tutorial: Building a Simple Interactive Web-Based Kanban Board

In the fast-paced world of software development and project management, staying organized and keeping track of tasks is crucial. Kanban boards, with their visual representation of workflow stages, have become a popular tool for teams of all sizes. This tutorial will guide you through building a simple, interactive web-based Kanban board using TypeScript, providing a solid foundation for understanding the core concepts and implementing more advanced features.

Why Build a Kanban Board?

Kanban boards offer several benefits:

  • Improved Visualization: They provide a clear, at-a-glance view of the project’s progress.
  • Enhanced Workflow Management: They help you identify bottlenecks and optimize your workflow.
  • Increased Team Collaboration: They facilitate better communication and collaboration among team members.
  • Increased Productivity: By focusing on one task at a time, Kanban boards help increase productivity.

Building one yourself is a fantastic way to learn about front-end development, TypeScript, and the principles of Kanban. You’ll gain practical experience with data structures, event handling, and UI manipulation, all while creating a useful tool.

Setting Up Your Development Environment

Before we dive into the code, let’s set up our development environment. You’ll need:

  • Node.js and npm (or yarn): For managing dependencies and running the development server.
  • A Code Editor: Visual Studio Code (VS Code) is highly recommended due to its excellent TypeScript support.
  • Basic HTML, CSS, and JavaScript knowledge: While this tutorial focuses on TypeScript, understanding these fundamentals will be helpful.

Let’s create a new project directory and initialize it with npm:

mkdir kanban-board
cd kanban-board
npm init -y

Next, install TypeScript and a few other necessary packages:

npm install typescript --save-dev
npm install --save-dev @types/node
npm install --save-dev parcel-bundler

We’re using Parcel as our bundler to simplify the build process. Parcel handles the bundling of our TypeScript code, HTML, CSS, and any assets like images.

Create a `tsconfig.json` file in your project root. This file configures the TypeScript compiler. Here’s a basic configuration:

{
  "compilerOptions": {
    "outDir": "dist",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom"],
    "sourceMap": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*"]
}

Create a `src` directory to hold your source files. Inside `src`, create an `index.html` file and a `index.ts` file.

Building the HTML Structure

Let’s start with the basic HTML structure in `src/index.html`:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kanban Board</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="app"></div>
    <script src="index.ts"></script>
</body>
</html>

This sets up the basic HTML with a title, a link to a stylesheet (`style.css`), and a script tag that points to our TypeScript file (`index.ts`). We also have a `div` with the id “app”, which will serve as the container for our Kanban board.

Styling with CSS

Create a `src/style.css` file and add some basic styles to make the board visually appealing:

body {
    font-family: sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f0f0f0;
}

#app {
    display: flex;
    justify-content: space-around;
    padding: 20px;
}

.column {
    background-color: #fff;
    border-radius: 5px;
    padding: 10px;
    width: 300px;
    min-height: 100px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.column h2 {
    text-align: center;
    margin-bottom: 10px;
}

.task {
    background-color: #f9f9f9;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 10px;
    margin-bottom: 10px;
    cursor: grab;
}

.task:active {
    cursor: grabbing;
}

This CSS provides basic styling for the body, the app container, the columns, and the tasks. Feel free to customize the styles to your liking.

Writing the TypeScript Code

Now, let’s write the core TypeScript logic in `src/index.ts`. We’ll break this down into several steps.

1. Defining Data Structures

First, define the data structures for our tasks and columns. This is where TypeScript’s type system comes in handy.

interface Task {
    id: string;
    title: string;
    description: string;
}

interface Column {
    id: string;
    title: string;
    tasks: Task[];
}

Here, we define two interfaces: `Task` and `Column`. The `Task` interface has properties for `id`, `title`, and `description`. The `Column` interface has properties for `id`, `title`, and an array of `Task` objects.

2. Initializing Data

Let’s create some initial data for our Kanban board. In a real application, you might fetch this data from a database.

const initialColumns: Column[] = [
    {
        id: 'todo',
        title: 'To Do',
        tasks: [
            { id: 'task1', title: 'Implement Kanban Board', description: 'Create the basic structure of the Kanban board.' },
            { id: 'task2', title: 'Add Drag and Drop', description: 'Implement drag and drop functionality for tasks.' },
        ],
    },
    {
        id: 'inProgress',
        title: 'In Progress',
        tasks: [],
    },
    {
        id: 'done',
        title: 'Done',
        tasks: [],
    },
];

let columns: Column[] = initialColumns;

We create an array of `Column` objects, each with a title and an array of tasks. We initialize the `columns` variable with these initial values.

3. Rendering the Board

Next, let’s write a function to render the Kanban board in the HTML.

function renderBoard(): void {
    const appElement = document.getElementById('app');
    if (!appElement) {
        console.error('App element not found!');
        return;
    }

    appElement.innerHTML = ''; // Clear the board

    columns.forEach(column => {
        const columnElement = document.createElement('div');
        columnElement.classList.add('column');
        columnElement.dataset.columnId = column.id;

        const columnTitle = document.createElement('h2');
        columnTitle.textContent = column.title;
        columnElement.appendChild(columnTitle);

        column.tasks.forEach(task => {
            const taskElement = document.createElement('div');
            taskElement.classList.add('task');
            taskElement.dataset.taskId = task.id;
            taskElement.textContent = task.title;

            // Add drag and drop attributes
            taskElement.draggable = true;

            columnElement.appendChild(taskElement);
        });

        appElement.appendChild(columnElement);
    });
}

This `renderBoard` function does the following:

  • Gets the `app` element from the DOM.
  • Clears the content of the `app` element.
  • Iterates through the `columns` array.
  • For each column, creates a `div` element with the class “column” and adds the column title.
  • Iterates through the tasks in each column.
  • For each task, creates a `div` element with the class “task” and adds the task title.
  • Appends the column and task elements to the `app` element.

4. Implementing Drag and Drop

Now, let’s add the drag and drop functionality. This is a crucial part of a Kanban board.

let draggedTask: HTMLElement | null = null;
let sourceColumnId: string | null = null;

function addDragAndDropListeners(): void {
    document.addEventListener('dragstart', (event) => {
        draggedTask = event.target as HTMLElement;
        sourceColumnId = (event.target as HTMLElement).closest('.column')?.dataset.columnId || null;
        event.dataTransfer?.setData('text/plain', draggedTask?.dataset.taskId || '');
    });

    document.addEventListener('dragover', (event) => {
        event.preventDefault(); // Required to allow dropping
    });

    document.addEventListener('drop', (event) => {
        event.preventDefault();
        if (!draggedTask || !sourceColumnId) return;

        const targetColumnId = (event.target as HTMLElement).closest('.column')?.dataset.columnId;
        if (!targetColumnId) return;

        const taskId = draggedTask.dataset.taskId;
        if (!taskId) return;

        // Remove task from source column
        columns = columns.map(column => {
            if (column.id === sourceColumnId) {
                return {
                    ...column,
                    tasks: column.tasks.filter(task => task.id !== taskId),
                };
            } else {
                return column;
            }
        });

        // Add task to target column
        columns = columns.map(column => {
            if (column.id === targetColumnId) {
                const taskToMove = initialColumns.flatMap(col => col.tasks).find(task => task.id === taskId);
                if (taskToMove) {
                    return {
                        ...column,
                        tasks: [...column.tasks, taskToMove],
                    };
                } else {
                    return column;
                }
            } else {
                return column;
            }
        });

        renderBoard(); // Re-render the board
    });
}

Let’s break down this code:

  • `draggedTask` and `sourceColumnId`: These variables store the currently dragged task element and the ID of the column it originated from.
  • `addDragAndDropListeners()`: This function adds the event listeners for drag and drop.
  • `dragstart` event: This event is fired when the user starts dragging a task. We set the `draggedTask` and `sourceColumnId` variables and use `event.dataTransfer.setData` to store the task ID.
  • `dragover` event: This event is fired when the dragged task is over a valid drop target. We call `event.preventDefault()` to allow dropping.
  • `drop` event: This event is fired when the user drops the task. We get the target column ID, remove the task from the source column, add it to the target column, and then re-render the board.

5. Initializing the Board

Finally, let’s initialize the board by calling `renderBoard()` and `addDragAndDropListeners()`:

renderBoard();
addDragAndDropListeners();

This ensures that the board is rendered when the page loads and that the drag-and-drop functionality is enabled.

Putting It All Together

Here’s the complete `src/index.ts` file:

interface Task {
    id: string;
    title: string;
    description: string;
}

interface Column {
    id: string;
    title: string;
    tasks: Task[];
}

const initialColumns: Column[] = [
    {
        id: 'todo',
        title: 'To Do',
        tasks: [
            { id: 'task1', title: 'Implement Kanban Board', description: 'Create the basic structure of the Kanban board.' },
            { id: 'task2', title: 'Add Drag and Drop', description: 'Implement drag and drop functionality for tasks.' },
        ],
    },
    {
        id: 'inProgress',
        title: 'In Progress',
        tasks: [],
    },
    {
        id: 'done',
        title: 'Done',
        tasks: [],
    },
];

let columns: Column[] = initialColumns;

function renderBoard(): void {
    const appElement = document.getElementById('app');
    if (!appElement) {
        console.error('App element not found!');
        return;
    }

    appElement.innerHTML = ''; // Clear the board

    columns.forEach(column => {
        const columnElement = document.createElement('div');
        columnElement.classList.add('column');
        columnElement.dataset.columnId = column.id;

        const columnTitle = document.createElement('h2');
        columnTitle.textContent = column.title;
        columnElement.appendChild(columnTitle);

        column.tasks.forEach(task => {
            const taskElement = document.createElement('div');
            taskElement.classList.add('task');
            taskElement.dataset.taskId = task.id;
            taskElement.textContent = task.title;

            // Add drag and drop attributes
            taskElement.draggable = true;

            columnElement.appendChild(taskElement);
        });

        appElement.appendChild(columnElement);
    });
}

let draggedTask: HTMLElement | null = null;
let sourceColumnId: string | null = null;

function addDragAndDropListeners(): void {
    document.addEventListener('dragstart', (event) => {
        draggedTask = event.target as HTMLElement;
        sourceColumnId = (event.target as HTMLElement).closest('.column')?.dataset.columnId || null;
        event.dataTransfer?.setData('text/plain', draggedTask?.dataset.taskId || '');
    });

    document.addEventListener('dragover', (event) => {
        event.preventDefault(); // Required to allow dropping
    });

    document.addEventListener('drop', (event) => {
        event.preventDefault();
        if (!draggedTask || !sourceColumnId) return;

        const targetColumnId = (event.target as HTMLElement).closest('.column')?.dataset.columnId;
        if (!targetColumnId) return;

        const taskId = draggedTask.dataset.taskId;
        if (!taskId) return;

        // Remove task from source column
        columns = columns.map(column => {
            if (column.id === sourceColumnId) {
                return {
                    ...column,
                    tasks: column.tasks.filter(task => task.id !== taskId),
                };
            } else {
                return column;
            }
        });

        // Add task to target column
        columns = columns.map(column => {
            if (column.id === targetColumnId) {
                const taskToMove = initialColumns.flatMap(col => col.tasks).find(task => task.id === taskId);
                if (taskToMove) {
                    return {
                        ...column,
                        tasks: [...column.tasks, taskToMove],
                    };
                } else {
                    return column;
                }
            } else {
                return column;
            }
        });

        renderBoard(); // Re-render the board
    });
}

renderBoard();
addDragAndDropListeners();

To run your Kanban board, build your project using Parcel:

npx parcel src/index.html

This will create a `dist` directory containing the bundled HTML, CSS, and JavaScript files. Open `dist/index.html` in your browser to see your Kanban board in action.

Common Mistakes and How to Fix Them

1. Not Preventing Default Dragover Behavior

Mistake: Forgetting to call `event.preventDefault()` in the `dragover` event listener.

Fix: Without `event.preventDefault()`, the browser will not allow the drop operation. Make sure to include it in your `dragover` event listener.

document.addEventListener('dragover', (event) => {
    event.preventDefault(); // This is crucial!
});

2. Incorrectly Handling Task Removal/Addition

Mistake: Not updating the `columns` array correctly when moving tasks between columns.

Fix: Ensure that you correctly remove the task from the source column and add it to the target column. Use methods like `filter` and `map` to create new arrays with the updated data, rather than modifying the original array directly (which can lead to unexpected behavior). The code in the `drop` event listener in the example above demonstrates how to do this correctly.

3. Not Re-rendering the Board After Changes

Mistake: Failing to call `renderBoard()` after updating the `columns` array.

Fix: After moving a task, you must re-render the board to reflect the changes in the UI. Make sure to call `renderBoard()` at the end of your `drop` event listener.

4. Incorrectly Setting `draggable` Attribute

Mistake: Not setting the `draggable` attribute to `true` on the task elements.

Fix: This attribute is essential for enabling the drag functionality. Make sure to set `taskElement.draggable = true;` when creating the task elements.

Enhancements and Next Steps

This tutorial provides a basic foundation. Here are some ways you can enhance your Kanban board:

  • Adding Tasks: Implement a form to allow users to add new tasks to the board.
  • Editing Tasks: Allow users to edit task titles and descriptions.
  • Deleting Tasks: Implement a way to delete tasks.
  • Persisting Data: Use local storage or a backend database to store the board’s data so that it persists across sessions.
  • Advanced Drag and Drop: Implement more sophisticated drag and drop behavior, such as reordering tasks within a column.
  • User Interface Improvements: Add more styling, animations, and user feedback to enhance the user experience.
  • Adding new columns: Allow users to add or remove columns.

Key Takeaways

  • TypeScript is a powerful tool for building robust and maintainable front-end applications.
  • Kanban boards are an effective way to visualize and manage workflows.
  • Understanding data structures, event handling, and DOM manipulation is essential for building interactive web applications.
  • Drag and drop functionality can be implemented using the HTML5 Drag and Drop API.
  • Parcel simplifies the build process for front-end projects.

FAQ

1. How can I add new tasks to the board?

To add new tasks, you would typically:

  1. Create a form with input fields for the task title and description.
  2. Add an event listener to the form’s submit button.
  3. When the form is submitted, create a new `Task` object with a unique ID.
  4. Add the new task to the appropriate column in your `columns` array.
  5. Re-render the board.

2. How can I save the board’s data so it persists across sessions?

You can use either:

  • Local Storage: Store the `columns` array as a JSON string in the browser’s local storage. When the page loads, retrieve the data from local storage and initialize your `columns` array.
  • A Backend Database: Send the `columns` data to a backend server (e.g., using an API). The server would then store the data in a database. When the page loads, fetch the data from the server.

3. How can I prevent tasks from being dragged outside of the columns?

You can add CSS styles to the column elements (e.g., `overflow: hidden;`) to restrict the dragging area. You might also need to adjust your drag and drop event listeners to ensure that the drop target is always within a column.

4. What are some good libraries for drag and drop functionality?

While the native HTML5 Drag and Drop API is sufficient for this tutorial, consider these libraries for more complex drag-and-drop interactions:

  • React DnD: A popular library for React applications.
  • SortableJS: A lightweight and flexible library for reorderable drag-and-drop lists.
  • Interact.js: A versatile library for handling drag and drop, resizing, and other interactions.

5. How can I improve the performance of my Kanban board?

Performance optimization includes:

  • Debouncing/Throttling: If you have frequent updates (e.g., real-time updates from a server), debounce or throttle the updates to avoid excessive re-renders.
  • Virtualization: If you have a large number of tasks, consider using virtualization to render only the visible tasks.
  • Optimized Rendering: Avoid unnecessary DOM manipulations.
  • Using Web Workers: For computationally intensive tasks, consider using Web Workers to perform them in the background.

Building a web-based Kanban board is a rewarding project that allows you to apply and solidify your TypeScript skills. By following this tutorial, you’ve gained a fundamental understanding of how to structure the application, implement drag-and-drop functionality, and manage data. The ability to create a functional and interactive Kanban board provides a valuable skill set for any front-end developer. As you explore the enhancements and continue to refine your project, you’ll find yourself not only building a useful tool, but also deepening your understanding of web development principles. The journey of building a Kanban board is a step forward in your development journey, one that will enrich your understanding of web development and provide a practical tool for organizing your tasks.