Frontend
Back to frontend track

General Courses

Todo App with Svelte, TypeScript, and TDD

Todo apps are a great way to learn a new language or framework. We teach this course with more than Svelte.

Adding Tasks

Let us add interactivity by letting the user add tasks to the list. We'll add a text input and a button. When the user clicks the button, we'll add the text from the input to the list of tasks.

Adding a Text Input and Button

First, add a text input and a button to the page. We'll add them just above the list of tasks.

src/routes/+page.svelte
<!-- Rest of the code omitted for brevity  -->
 
<div>
  <h1>Tasks</h1>
  <input />
  <button>Add</button>
  <ul>
    {#each tasks as task (task.id)}
      <li>{task.title}</li>
    {/each}
  </ul>
</div>
 
<!-- Rest of the code omitted for brevity  -->

Though this is good enough for now, we can and should make it more accessible by adding a label for the input and a for attribute on the label that matches the id of the input.

src/routes/+page.svelte
<!-- Rest of the code omitted for brevity  -->
 
<div>
  <h1>Tasks</h1>
  <label for='task-input'>Add Task: </label>
  <input id='task-input' />
  <button>Add</button>
  <ul>
    {#each tasks as task (task.id)}
      <li>{task.title}</li>
    {/each}
  </ul>
</div>
 
<!-- Rest of the code omitted for brevity  -->

Now, clicking on the label will focus on the input.

Adding a Task

The Add button doesn't do anything yet. Let's add a click handler to it that will log to the console for now.

src/routes/+page.svelte
<!-- Rest of the code omitted for brevity  -->
 
<div>
  <h1>Tasks</h1>
  <label for="task-input">Add Task: </label>
  <input id="task-input" />
  <button on:click={() => console.log('Add')}>Add</button>
  <ul>
    {#each tasks as task (task.id)}
      <li>{task.title}</li>
    {/each}
  </ul>
</div>
 
<!-- Rest of the code omitted for brevity  -->

Let's capture the value of the input and log it to the console. One of the ways to do this in Svelte is to bind a variable to track the value of the input.

All we need to do is add a bind:value attribute to the input and track it in the taskName variable.

src/routes/+page.svelte
<script lang="ts">
  // Some of the code omitted for brevity
 
  let taskName = ''; 
</script>
 
<div>
  <!-- Some of the code omitted for brevity  -->
 
  <input id="task-input" bind:value={taskName} />
 
  <!-- Some of the code omitted for brevity  -->
</div>

Now, we can update the button click handler to log the value of the input.

src/routes/+page.svelte
<!-- Rest of the code omitted for brevity  -->
 
<div>
  <!-- Some of the code omitted for brevity  -->
 
  <input id="task-input" bind:value={taskName} />
  <button on:click={() => console.log(taskName)}>Add</button>
 
  <!-- Some of the code omitted for brevity  -->
</div>

Before we can even add tasks to the list, we have to update our current tasks variable that holds our list of tasks to be mutable a.k.a a let; otherwise, the list will not update when we add a task.

src/routes/+page.svelte
<script lang="ts">
  // Some of the code omitted for brevity
 
  let tasks: Task[] = [
    {
      id: 1,
      title: 'Learn Svelte',
      isCompleted: true,
      priority: 'p1',
    },
  ];
 
  // Some of the code omitted for brevity
</script>
 
<!-- Rest of the code omitted for brevity  -->

We can add a task to the list by updating the tasks state variable. We can push the new task onto the end of the array but to re-render the list we must reassign the tasks array to itself. This is how reactivity in Svelte works, through assignments. This would have been natural if it was a primitive data type like a string or number. This is not the case with Objects and Arrays.

src/routes/+page.svelte
<!-- Rest of the code omitted for brevity  -->
 
<div>
  <!-- Some of the code omitted for brevity  -->
 
  <button
    on:click={() => {
      tasks.push({ id: new Date().getTime(), title: taskName, isCompleted: false });
      tasks = tasks;
    }}>Add</button
  >
 
  <!-- Some of the code omitted for brevity  -->
</div>

If that felt weird, there is a more meaningful way to add tasks to the list.

src/routes/+page.svelte
<!-- Rest of the code omitted for brevity  -->
 
<div>
  <!-- Some of the code omitted for brevity  -->
 
  <button
    on:click={() => {
      tasks = [
        ...tasks,
        { id: new Date().getTime(), title: taskName, isCompleted: false },
      ];
    }}>Add</button
  >
 
  <!-- Some of the code omitted for brevity  -->
</div>

Type in the input and hit the Add button, we should be able to add tasks to the list!

Our Svelte file is getting a bit long and messy, so let's separate the button's on:click handler into its function.

src/routes/+page.svelte
<script lang="ts">
  type Priority = 'p1' | 'p2' | 'p3';
 
  type Task = {
    id: number;
    title: string;
    isCompleted: boolean;
    priority?: Priority;
  };
 
  let tasks: Task[] = [
    {
      id: 1,
      title: 'Learn Svelte',
      isCompleted: true,
      priority: 'p1',
    },
  ];
 
  let taskName = '';
 
  const addTask = () => {
    tasks = [
      ...tasks,
      { id: new Date().getTime(), title: taskName, isCompleted: false },
    ];
  };
</script>
 
<div>
  <h1>Tasks</h1>
  <label for="task-input">Add Task: </label>
  <input id="task-input" bind:value={taskName} />
  <button on:click={addTask}>Add</button>
  <ul>
    {#each tasks as task (task.id)}
      <li>{task.title}</li>
    {/each}
  </ul>
</div>

This is how your final Svelte code should look.

Awesome, but there are a ton of things that can go wrong when a user adds a task. For example, the user can add an empty task.

This gives us a good opportunity to learn about Testing. We'll cover testing in the subsequent sections.

At this point, your code should be a good match to the branch of the repository: 3-adding-tasks