Implementing Task Movement in a Kanban Board

Introduction to Task Movement in Kanban Boards

Welcome to the fourth lesson of our "Building A Kanban Board" course! In our previous lessons, we've set up our task store using Svelte's Runes API, created the visual components to display our tasks, and implemented a form to add new tasks. Now, we're ready to make our Kanban board truly functional by implementing task movement between columns.

Task movement is a core feature of any Kanban board. In real-world project management, tasks naturally progress through different stages of completion. A task might start in the "To Do" column, move to "In Progress" when someone begins working on it, and finally reach the "Done" column when completed. Without the ability to move tasks between these columns, our Kanban board would be little more than a static display of information.

Lesson Objectives

In this lesson, we'll implement this crucial functionality by:

  1. Adding a function to our task store that updates a task's status
  2. Creating a component that displays action buttons for moving tasks
  3. Enhancing our TaskCard component to handle status changes
  4. Connecting everything together to create a seamless user experience

By the end of this lesson, you'll be able to click on a task to expand it, see available actions based on its current status, and move it to a different column with a single click. This interaction pattern is intuitive and mirrors how real Kanban boards work, making our application not just visually appealing but also practically useful.

Let's get started by examining how we'll update our task store to support status changes!

Enhancing the Task Store with Status Updates

In our first lesson, we created a task store with an initial state and derived values for filtering tasks by status. In the third lesson, we added the ability to create new tasks. Now, we need to add functionality to update a task's status.

Let's examine the enhanced version of our taskStore.svelte.js file:

// Create initial state with $state
export const tasks = $state([
  { id: 1, title: 'Learn Svelte 5', description: 'Study the new Runes API', status: 'todo' },
  { id: 2, title: 'Design components', description: '', status: 'todo' },
  { id: 3, title: 'Build Kanban board', description: 'Create the main layout', status: 'inprogress' },
  { id: 4, title: 'Setup project', description: 'Initialize SvelteKit', status: 'done' }
]);

// Derive filtered tasks for each column
const todoTasks = $derived(tasks.filter(task => task.status === 'todo'));
const inProgressTasks = $derived(tasks.filter(task => task.status === 'inprogress'));
const doneTasks = $derived(tasks.filter(task => task.status === 'done'));

export const getTodoTasks = () => todoTasks;
export const getInProgressTasks = () => inProgressTasks;
export const getDoneTasks = () => doneTasks;

// Function to add a new task
export function addTask(title, description = '') {
  const newTask = {
    id: Date.now(),
    title,
    description,
    status: 'todo'
  };
  
  tasks.push(newTask);
  return newTask;
}

// Function to update task status
export function updateTaskStatus(id, newStatus) {
  const taskIndex = tasks.findIndex(task => task.id === id);
  if (taskIndex !== -1) {
    tasks[taskIndex].status = newStatus;
  }
}

Understanding the updateTaskStatus Function

The key addition here is the updateTaskStatus function, which takes two parameters:

  • id: The unique identifier of the task to update
  • newStatus: The new status to assign to the task (e.g., 'todo', 'inprogress', or 'done')

Let's break down how this function works:

  1. First, it uses the findIndex method to locate the task with the specified ID in our tasks array. This method returns the index of the first element that satisfies the provided testing function, or -1 if no element is found.

  2. Then, it checks if a task with the given ID was found (i.e., if taskIndex is not -1).

  3. If a matching task was found, it updates the status of that task by directly modifying the status property of the task object at the found index.

Reactivity Benefits for Task Movement

This direct modification of the task's status property works because our tasks array is reactive (created with $state). When we modify a property of an object within this array, Svelte's reactivity system detects the change and automatically updates any parts of the UI that depend on this data.

This is particularly powerful because our derived values (todoTasks, inProgressTasks, and doneTasks) are calculated based on the status of each task. When a task's status changes, these derived values automatically update, which in turn updates the UI to show the task in its new column.

For example, if we move a task from "To Do" to "In Progress", the following happens automatically:

  1. The task is removed from the todoTasks array
  2. The task is added to the inProgressTasks array
  3. The UI updates to show the task in the "In Progress" column

All of this happens without us having to manually update multiple arrays or trigger UI refreshes. This is the power of Svelte's reactivity system — we declare our dependencies and let Svelte handle the updates.

Now that we have the functionality to update a task's status, let's create a component that provides a user interface for this functionality.

Building the TaskActions Component

To provide a user-friendly way to move tasks between columns, we'll create a new component called TaskActions.svelte. This component will display buttons for moving a task to different statuses, based on its current status.

Here's the code for our TaskActions.svelte component:

<script>
  let { status, onStatusChange } = $props();
</script>

<div class="task-actions">
  {#if status !== 'todo'}
    <button onclick={() => onStatusChange('todo')}>
      Move to To Do
    </button>
  {/if}
  {#if status !== 'inprogress'}
    <button onclick={() => onStatusChange('inprogress')}>
      Move to In Progress
    </button>
  {/if}
  {#if status !== 'done'}
    <button onclick={() => onStatusChange('done')}>
      Move to Done
    </button>
  {/if}
</div>

<style>
  .task-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-top: 0.5rem;
  }
  
  button {
    font-size: 0.75rem;
    padding: 0.25rem 0.5rem;
    background: #e2e8f0;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  
  button:hover {
    background: #cbd5e0;
  }
</style>

Component Structure and Props

Let's examine this component in detail:

In the script section, we're using the $props() rune to define the props that this component accepts:

  • status: The current status of the task
  • onStatusChange: A callback function that will be called when the user clicks a button to change the task's status

The $props() rune is a new feature in Svelte 5 that provides a cleaner way to define component props. It returns an object with the props passed to the component, which we destructure to get the specific props we need.

Conditional Rendering of Action Buttons

In the markup section, we're creating a div that contains buttons for moving the task to different statuses. We're using conditional rendering with {#if} blocks to only show buttons for statuses that are different from the task's current status. This prevents users from moving a task to the status it's already in, which would be redundant.

Each button has an onclick handler that calls the onStatusChange callback with the new status as an argument. This callback will be provided by the parent component (which we'll create next) and will handle the actual status update.

Styling the Action Buttons

In the style section, we're using flexbox to arrange the buttons in a row, with wrapping enabled in case there are too many buttons to fit on one line. We're also styling the buttons to be small and subtle, with a hover effect that changes the background color to provide visual feedback.

This component follows the principle of separation of concerns:

  • It's responsible only for displaying the UI for changing a task's status
  • It doesn't know how to actually update the status; it just calls a callback provided by the parent
  • It doesn't know about the task store or any other parts of the application

This makes the component reusable and maintainable. If we wanted to change how task statuses are updated, we would only need to modify the parent component, not this one.

Now that we have our TaskActions component, let's see how to integrate it into our TaskCard component to provide a complete user interface for task management.

Implementing Expandable TaskCards

Now that we have our TaskActions component, we need to enhance our TaskCard component to display these actions when a user interacts with a task. We'll also add the ability to expand and collapse a task card to show or hide additional details and actions.

Here's the updated code for our TaskCard.svelte component:

<script>
  import TaskActions from './TaskActions.svelte';
  import { updateTaskStatus } from '$lib/taskStore.svelte.js';
  
  let { id, title, description = '', status } = $props();
  let isExpanded = $state(false);
  
  function toggleExpand() {
    isExpanded = !isExpanded;
  }
  
  function handleStatusChange(newStatus) {
    updateTaskStatus(id, newStatus);
  }
</script>

<div 
  class="task-card" 
  tabindex="0" 
  onclick={toggleExpand}
>
  <h3>{title}</h3>
  {#if description && isExpanded}
    <p class="description">{description}</p>
  {/if}
  
  {#if isExpanded}
    <TaskActions 
      status={status} 
      onStatusChange={handleStatusChange} 
    />
  {/if}
</div>

<style>
  .task-card {
    background: white;
    padding: 0.75rem;
    border-radius: 6px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    margin-bottom: 0.5rem;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
    cursor: pointer;
  }

  .task-card:hover, .task-card:focus {
    transform: translateY(-2px);
    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
  }

  .task-card:focus {
    outline: 2px solid #4299e1;
    outline-offset: 2px;
  }

  h3 {
    margin-top: 0;
    margin-bottom: 0.5rem;
    font-size: 1rem;
  }

  .description {
    margin: 0 0 0.75rem 0;
    font-size: 0.875rem;
    color: #4a5568;
  }
</style>

Enhanced TaskCard Features

Let's break down the changes we've made to this component:

In the script section, we've added several new features:

  1. We're importing the TaskActions component and the updateTaskStatus function from our task store
  2. We've expanded our props to include id and status, which we'll need for task movement
  3. We've added a new reactive variable isExpanded using the $state rune, initialized to false to track whether the task card is expanded or collapsed
  4. We've added two functions:
    • toggleExpand: Toggles the value of isExpanded when called
    • handleStatusChange: Calls the updateTaskStatus function from our task store with the task's ID and the new status

Interactive UI Elements

In the markup section, we've made several changes:

  1. We've added a tabindex="0" attribute to the task card div, which makes it focusable with keyboard navigation (improving accessibility)
  2. We've added an onclick handler that calls the toggleExpand function when the task card is clicked
  3. We've wrapped the description in a conditional block that only renders it if the description is not empty and the card is expanded
  4. We've added a conditional block that renders the TaskActions component if the card is expanded, passing the task's current status and our handleStatusChange function as props

Visual Feedback through Styling

In the style section, we've added styles for the expanded state and improved the hover and focus styles to provide better visual feedback. The subtle animations help users understand that the cards are interactive and provide a more polished user experience.

This implementation creates an intuitive user interface:

  1. By default, task cards show only the title, keeping the UI clean and compact
  2. When a user clicks on a card, it expands to show the description (if any) and action buttons
  3. The user can then click an action button to move the task to a different status
  4. The user can collapse the card by clicking it again

This pattern of progressive disclosure (showing more details and actions only when needed) is common in modern UIs and helps prevent information overload.

How Status Changes Flow Through the Application

Now that we have our TaskActions component and our enhanced TaskCard component, let's examine how status changes are handled throughout our application.

The Chain of Events for Task Movement

When a user clicks on a task card and then clicks one of the action buttons, a chain of events occurs:

  1. The click on the action button triggers the onclick handler in the TaskActions component, which calls the onStatusChange callback with the new status as an argument.

  2. This callback is actually the handleStatusChange function from the TaskCard component, which receives the new status and calls the updateTaskStatus function from our task store with the task's ID and the new status.

  3. The updateTaskStatus function finds the task with the specified ID in our tasks array and updates its status.

  4. Because our tasks array is reactive (created with $state), this update triggers a re-evaluation of our derived values (todoTasks, inProgressTasks, and doneTasks).

  5. The UI automatically updates to show the task in its new column.

This chain of events demonstrates the power of Svelte's reactivity system and the component-based architecture we've built. Each component has a specific responsibility, and they work together to create a seamless user experience.

A Concrete Example

Let's look at a concrete example to illustrate this process:

Imagine we have a task with ID 2, title "Design components", and status "todo". It's currently displayed in the "To Do" column of our Kanban board.

  1. The user clicks on the task card, which expands to show the action buttons
  2. The user clicks the "Move to In Progress" button
  3. The onclick handler in the TaskActions component calls onStatusChange('inprogress')
  4. The handleStatusChange function in the TaskCard component calls updateTaskStatus(2, 'inprogress')
  5. The updateTaskStatus function finds the task with ID 2 and changes its status to "inprogress"
  6. The todoTasks derived value is re-evaluated and no longer includes this task
  7. The inProgressTasks derived value is re-evaluated and now includes this task
  8. The UI updates to show the task in the "In Progress" column

All of this happens automatically, without us having to manually update arrays or trigger UI refreshes. This is the beauty of reactive programming with Svelte — we declare our dependencies and let the framework handle the updates.

Now, let's see how our Board component ties everything together to create a complete Kanban board.

Connecting Components with the Board

Our Board component is responsible for rendering the three columns of our Kanban board and populating them with task cards. Let's examine the updated code for our Board.svelte component:

<script>
  import Column from './Column.svelte';
  import TaskCard from './TaskCard.svelte';
  import { getTodoTasks, getInProgressTasks, getDoneTasks } from '$lib/taskStore.svelte.js';
</script>

<div class="board">
  <Column title="To Do">
    {#each getTodoTasks() as task (task.id)}
      <TaskCard 
        id={task.id}
        title={task.title} 
        description={task.description}
        status={task.status}
      />
    {/each}
  </Column>
  <Column title="In Progress">
    {#each getInProgressTasks() as task (task.id)}
      <TaskCard 
        id={task.id}
        title={task.title}
        description={task.description}
        status={task.status}
      />
    {/each}
  </Column>
  <Column title="Done">
    {#each getDoneTasks() as task (task.id)}
      <TaskCard 
        id={task.id}
        title={task.title}
        description={task.description}
        status={task.status}
      />
    {/each}
  </Column>
</div>

<style>
  .board {
    display: flex;
    gap: 1rem;
  }
</style>

Board Structure and Component Composition

In this component, we're importing the Column and TaskCard components, as well as the functions to get tasks by status from our task store. We're rendering three Column components, each with a title corresponding to a task status. Inside each column, we're using the {#each} block to iterate over the tasks for that status and render a TaskCard for each one.

We pass all necessary props to each TaskCard, including:

  • id: Used to identify the task when updating its status
  • title: The task's title to display
  • description: The task's description (if any)
  • status: The current status of the task, used by the action buttons

This setup creates a dynamic Kanban board where tasks can be moved between columns by changing their status. The reactivity of Svelte ensures that the UI updates automatically whenever a task's status changes, providing a seamless user experience.

Summary

In this lesson, we've implemented the core functionality that makes a Kanban board truly useful: the ability to move tasks between columns. Here's what we've accomplished:

  • Enhanced our task store with the updateTaskStatus() function that modifies a task's status based on its ID
  • Created a TaskActions component that displays context-appropriate buttons for moving tasks between columns
  • Upgraded our TaskCard component with expandable functionality that reveals task descriptions and action buttons when clicked
  • Connected everything together through Svelte's reactive system, ensuring the UI automatically updates when task statuses change

This implementation demonstrates the power of reactive programming with Svelte. By simply updating a task's status property, our derived column values automatically recalculate, and the task visually moves to its new column without any manual DOM manipulation.

The component-based architecture we've built maintains a clear separation of concerns where each component has a specific responsibility, creating a clean and maintainable codebase.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal