Creating a Form for Task Addition in Svelte

Introduction to Task Forms in Kanban Applications

Welcome to the third lesson of our "Building A Kanban Board" course! In our previous lessons, we set up our task store using Svelte's Runes API and created the visual components to display our tasks in a Kanban board layout. Now it's time to make our application interactive by allowing users to add new tasks.

A key feature of any task management application is the ability for users to create new tasks. Without this functionality, our Kanban board would be static and not very useful in real-world scenarios. By adding a form component, we'll transform our application from a simple display of tasks to an interactive tool that users can use to manage their work.

Lesson Objectives

By the end of this lesson, you'll understand how to:

  • Enhance our task store with functionality to add new tasks
  • Create a form component with proper validation
  • Manage form state using Svelte's Runes
  • Connect user input to our application state

Let's get started by adding the necessary functionality to our task store!

Enhancing the Task Store with Add Functionality

In our first lesson, we created a task store that manages our application's state. Now, we need to enhance this store with the ability to add new tasks. Let's update our taskStore.svelte.js file to include this functionality:

// 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;
}

Understanding the addTask Function

The key addition here is the addTask function, which takes a title and an optional description as parameters. Let's break down what this function does:

  1. It creates a new task object with a unique ID (using Date.now() to generate a timestamp), the provided title and description, and a default status of "todo."
  2. It adds this new task to our tasks array using the push method.
  3. It returns the newly created task, which can be useful if we need to reference it later.

Notice that we're using Date.now() to generate a unique ID for each task. This is a simple approach that works well for our purposes, but in a production application, you might want to use a more robust ID generation method, such as a UUID library.

We're setting the default status to "todo" because new tasks typically start in the "To Do" column of a Kanban board. This makes sense from a user experience perspective, as tasks usually begin in the planning stage before moving to "In Progress" and eventually "Done."

Because our tasks array is reactive (created with $state), any changes to it will automatically trigger updates to our derived values (todoTasks, inProgressTasks, and doneTasks). This means that when we add a new task, it will automatically appear in the appropriate column of our Kanban board without any additional code.

Now that we have the functionality to add new tasks, let's create a form component that users can interact with.

Building the TaskForm Component Structure

Let's create a new file called TaskForm.svelte in the src/components directory. This component will contain a form with inputs for the task title and description:

<script>
  import { addTask } from '$lib/taskStore.svelte.js';
  
  let title = $state('');
  let description = $state('');
  const characterCount = $derived(description.length);
  
  function handleSubmit(e) {
    e.preventDefault();
    if (title.trim()) {
      addTask(title, description);
      title = '';
      description = '';
    }
  }
</script>

<form onsubmit={handleSubmit} class="task-form">
  <div class="form-group">
    <label for="title">Title</label>
    <input 
      id="title"
      type="text" 
      placeholder="Enter task title" 
      bind:value={title}
      required
    />
  </div>
  
  <div class="form-group">
    <label for="description">
      Description <span class="char-count">{characterCount}/200</span>
    </label>
    <textarea 
      id="description"
      placeholder="Add details (optional)" 
      bind:value={description}
      rows="3"
      maxlength="200"
    ></textarea>
  </div>
  
  <button type="submit">Add Task</button>
</form>

Component Structure Overview

Let's examine the structure of this component:

In the script section, we're importing the addTask function from our task store. We're also creating two reactive variables using $state: title and description, which will hold the values of our form inputs. Additionally, we're creating a derived value characterCount that tracks the length of the description, which we'll use to display a character counter.

Form Submission Handler

We've defined a handleSubmit function that will be called when the form is submitted. This function:

  • Prevents the default form submission behavior
  • Checks if the title is not empty (after trimming whitespace)
  • If valid, calls the addTask function with the title and description values
  • After adding the task, resets the form inputs to empty strings

Form Structure

In the markup section, we're creating a form with the onsubmit attribute set to our handleSubmit function. Inside the form, we have two form groups:

  1. Title Input Group: Contains a label and an input field for the task title. We're using the bind:value directive to create a two-way binding between the input value and our title variable. We've also added the required attribute to ensure the user provides a title.

  2. Description Input Group: Contains a label and a textarea for the task description. Again, we're using bind:value to create a two-way binding with our description variable. We've also added attributes to limit the textarea to 3 rows and a maximum of 200 characters.

The label for the description includes a span that displays the current character count and the maximum allowed characters. This provides immediate feedback to the user about how much text they can still enter.

Finally, we have a submit button that triggers the form submission when clicked.

Form Design Best Practices

This structure follows best practices for form design:

  • Each input has an associated label with a matching for attribute, which improves accessibility
  • We're using semantic HTML elements (form, label, input, textarea, button), which helps with accessibility and SEO
  • We're providing placeholder text to guide the user on what to enter
  • We're validating input (requiring a title) to ensure data quality
  • We're providing feedback (character count) to help the user understand constraints

Now that we have the basic structure of our form, let's look at how we're managing its state.

Managing Form State with Runes

Reactive State and Two-Way Binding

In our form markup, we're using the bind:value directive to create a two-way binding between our form inputs and these state variables:

<input 
  id="title"
  type="text" 
  placeholder="Enter task title" 
  bind:value={title}
  required
/>

<textarea 
  id="description"
  placeholder="Add details (optional)" 
  bind:value={description}
  rows="3"
  maxlength="200"
></textarea>

This means that when the user types in these inputs, our state variables will automatically update. And if we programmatically change these variables (as we do after submitting the form), the input values will update to reflect the new state.

We're also using our derived characterCount value in the UI to display a character counter:

<label for="description">
  Description <span class="char-count">{characterCount}/200</span>
</label>

This provides immediate feedback to the user about how many characters they've entered and how many they can still enter. Because characterCount is derived from description, it will automatically update as the user types, without any additional code.

This is a great example of how Svelte's reactivity system makes it easy to create dynamic, responsive UIs with minimal code. By declaring our dependencies and letting Svelte handle the updates, we can focus on the logic of our application rather than the mechanics of updating the UI.

Handling Form Submission

Now let's look at how we handle form submission in our TaskForm component:

function handleSubmit(e) {
  e.preventDefault();
  if (title.trim()) {
    addTask(title, description);
    title = '';
    description = '';
  }
}

This function is called when the form is submitted by clicking the submit button or pressing Enter in the title input field. (Note: pressing Enter in the description textarea won't submit the form since it's used for adding new lines.) Let's break down what this function does:

  1. e.preventDefault(): This prevents the default form submission behavior, which would cause the page to reload. Instead, we want to handle the submission in JavaScript.

  2. if (title.trim()): We check if the title is not empty after trimming any whitespace. This is a simple validation to ensure we don't add tasks with empty titles.

  3. addTask(title, description): If the title is valid, we call the addTask function from our task store, passing the title and description values.

  4. title = ''; description = '';: After adding the task, we reset the form inputs to empty strings. This provides a clear visual indication to the user that their task has been added and prepares the form for the next entry.

In our form markup, we connect this function to the form's submit event:

<form onsubmit={handleSubmit} class="task-form">
  <!-- form content -->
</form>

Form Submission and Validation

By using the onsubmit attribute, we ensure that our function is called whenever the form is submitted, regardless of how the submission is triggered (button click or Enter key).

This pattern of form handling is common in modern web applications:

  1. Prevent the default form submission
  2. Validate the input
  3. Process the data (in this case, add a new task)
  4. Reset the form

By following this pattern, we create a smooth, intuitive user experience that doesn't require page reloads and provides immediate feedback.

It's worth noting that we're doing minimal validation here (just checking if the title is not empty). In a production application, you might want to add more robust validation, such as checking for duplicate task titles or validating the format of certain fields. However, for our purposes, this simple validation is sufficient.

Styling the Form Component

Now let's add some CSS to make our form look nice and be consistent with the rest of our application:

<style>
  .task-form {
    margin-bottom: 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 500px;
    margin-left: auto;
    margin-right: auto;
    background: #f8fafc;
    padding: 1rem;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  }
  
  .form-group {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
  }
  
  label {
    font-size: 0.875rem;
    font-weight: 500;
    color: #4a5568;
    display: flex;
    justify-content: space-between;
  }
  
  .char-count {
    font-size: 0.75rem;
    color: #718096;
    font-weight: normal;
  }
  
  input, textarea, button {
    padding: 0.5rem;
    border-radius: 4px;
    font-size: 1rem;
  }
  
  input, textarea {
    border: 1px solid #cbd5e0;
  }
  
  input:focus, textarea:focus {
    outline: none;
    border-color: #4299e1;
    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
  }
  
  button {
    background: #4299e1;
    color: white;
    border: none;
    cursor: pointer;
    font-weight: 500;
  }
  
  button:hover {
    background: #3182ce;
  }
</style>

Form Styling Elements

These styles create a visually appealing and user-friendly form:

  1. Form Container:

    • Light background, rounded corners, and a subtle shadow to make it stand out
    • Centered on the page with a limited width of 500 pixels for optimal readability
    • Flexbox arrangement for vertical element layout with consistent spacing
  2. Input Fields:

    • Labels styled to be smaller and lighter to create visual hierarchy
    • Character count styled even lighter and smaller for subtle feedback
    • Consistent padding, border-radius, and font size across inputs
    • Focus states that change border color and add a subtle glow for clear interaction feedback
  3. Submit Button:

    • Blue background with white text that stands out as a call to action
    • Hover effect that darkens the blue slightly to indicate interactivity
    • Styling that matches the overall form design while remaining prominent

These styles create a clean, modern look that's consistent with the rest of our application. The form is easy to read and use, with clear visual feedback for interactions like focusing on an input or hovering over the button.

Integrating the Form into our Application

To use our TaskForm component, we need to import it in our main page component and add it to the markup. Here's how we would update our +page.svelte file:

<script>
  import Board from '../components/Board.svelte';
  import TaskForm from '../components/TaskForm.svelte';
</script>

<div class="container">
  <h1>📝 My Kanban Board</h1>
  <TaskForm />
  <Board />
</div>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem;
  }

  h1 {
    text-align: center;
    margin-bottom: 1.5rem;
    color: #2d3748;
  }
</style>

Component Placement

By placing the TaskForm component above the Board component, we create a natural flow where users first see the form to add new tasks and then see the board with all tasks. This is a common pattern in user interface design that follows the natural workflow of first creating items and then viewing them.

Page Structure

The page layout is simple and focused:

  1. A container with a maximum width and centered on the page
  2. A title that clearly identifies the application
  3. The TaskForm component for adding new tasks
  4. The Board component for displaying tasks in columns

This structure creates a clean, focused user interface that guides the user through the process of managing their tasks.

Summary

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