Learn Svelte 3 by building a Todo List App

Why is Svelte gaining popularity, and do we really need another JavaScript framework? In this article, we’ll take a look the value proposition of Svelte compared to other mainstream frameworks, and get some hands on experience by building a todo list application

Svelte is not a new JavaScript framework with its first appearance on the scene coming around late 2016. But it only just caught my attention with the release of Svelte 3 which was discussed at length on Hacker News.

Initially, I was dismissive of the whole announcement framing it in my mind as “Yet Another JavaScript Framework”. On closer inspection however, I discovered that Svelte could actually be worth paying attention to as it brings some things to the table that are not present with popular frameworks like React or Vue.

The first thing you will notice is that Svelte is easy to pick up and start using right away, perhaps because there is no weird syntax to learn. It’s really just HTML, CSS and JavaScript with some helpers.

Another big difference is that, Svelte does not use a virtual DOM. Although the use of virtual DOM has often been touted as a reason why React is so efficient at, well, reacting, Svelte proves that this step is not necessary for high performance reactivity.

I’m not going to go too deep into how Svelte works under the hood in this article. Instead, I’m going to demonstrate how to build a simple todo list app with Svelte.

Here is a live demo of what we’ll be building.

Prerequisites

This tutorial is mostly targeted at developers who have prior experience with frameworks such as React or Vue. However, you should be able to follow through with it even if all you know is vanilla JavaScript or jQuery.

That being said, a basic understanding of the command line and git is a must. Additionally, you need a recent installation of Node.js and npm (or yarn) on your machine.

Grab the starter files

Clone this GitHub repository to your computer and cd into the created directory. Then run npm install from the project root to install all the dependencies specified in the package.json file.

Speaking of the package.json file, notice how Svelte is listed under devDependencies. This is because Svelte is involved only at compile time and not at runtime, converting your components into imperative code with excellent performance characteristics when building for production.

After installing the dependencies, you can start the development server by running npm run dev which should compile the app and make it accessible at http://localhost:5000.

If you look into the src folder, you will see two files: main.js is the entry point for the app, while App.svelte is the root component for the app that serves to bring all other components together. In main.js, the App component is imported and instantiated on the <body> element in public/index.html.

Anatomy of a Svelte component

Svelte components are composed in .svelte files. This is where all the markup, logic and styling for a particular component will be written in. Here’s how a typical Svelte component looks like:

<script>
  // component logic
</script>

<style>
  /* component specific styles */
</style>

<!-- component markup -->

That’s it! You do not need to learn any special syntax to write a Svelte component, just regular ol’ JavaScript, CSS and HTML, with a few Svelte-specific additions to the HTML syntax which should be easy enough to pick up.

As an aside, you might need to install an extension for your code editor to provide syntax highlighting for .svelte files. Vim users can try vim-svelte, while VS Code users can use Svelte for VS Code.

Create the application structure

Open up App.svelte and replace the <!-- component markup --> comment with the following code:

<main>
  <div class="container">
    <h1 class="app-title">todos</h1>
    <ul class="todo-list"></ul>
    <div class="empty-state">
      <svg class="checklist-icon"><use href="#checklist-icon"></use></svg>
      <h2 class="empty-state__title">Add your first todo</h2>
      <p class="empty-state__description">What do you want to get done today?</p>
    </div>
    <form>
      <input class="js-todo-input" type="text" aria-label="Enter a new todo item" placeholder="E.g. Build a web app" />
    </form>
  </div>
</main>

Nothing new to see here. Just basic HTML that describes the structure of our application. For simplicity, the styles for the app are placed in public/global.css while the svg icons used are defined in public/index.html.

Next, let’s figure out how to add a todo item and render it in the application.

Add a todo

Add the following code between the script tags in App.svelte as shown below:

<script>
let todoItems = [];
let newTodo = '';

function addTodo() {
  newTodo = newTodo.trim();
  if (!newTodo) return;

  const todo = {
      text: newTodo,
      checked: false,
      id: Date.now(),
  };

  todoItems = [...todoItems, todo];
  newTodo = '';
}
</script>

The todoItems array serves to hold our todo list items while newTodo represents the input of the user when adding a new todo item. When the addTodo function is invoked, a new todo object is constructed and appended to the todoItems array. The reason why I’ve opted to use the spread operator to construct a new array instead of using todoItems.push(todo) for example is because Svelte only updates the DOM on assignments.

There’s nothing strange about the above code, but how can we bind our markup to the logic? Let’s find out. Update the form element in the HTML as shown below:

<form on:submit|preventDefault={addTodo}>
  <input class="js-todo-input" type="text" aria-label="Enter a new todo item" placeholder="E.g. Build a web app" bind:value={newTodo}>
</form>

Svelte provides a handy way to bind functions to events using the on:event syntax while passing in a reference to a function defined in your JavaScript logic. DOM event handlers can have modifiers that can alter their behavior. For example, when handling a form submission on the client, you typically want to do event.preventDefault() to stop the browser from refreshing the page. Svelte provides a preventDefault modifier that does this for you before running your event handler. That’s what the |preventDefault part after on:submit is all about.

If you look at the <input> element, you will see a new bind:value attribute and the newTodo variable passed to it. This how two-way binding is achieved in Svelte. If you’re not familiar with this concept, it helps us update the newTodo variable with the value of the form input and also update the form input if the newTodo variable is updated programatically in the app logic. This is why setting newTodo to an empty string at the end of the addTodo function clears the form input.

Try it out. Enter a new todo item, and hit Enter. Behind the scenes, the todo item will be appended the todoItems array, and the form input is cleared. If you want to see this in action, log todoItems to the console at the end of addTodo().

Render the todo items

Once we append a new todo to todoItems, we want the page to be updated and the item rendered on the screen. We can do this easily in Svelte using a special each constuct in the HTML:

<ul class="todo-list">
  {#each todoItems as todo (todo.id)}
    <li class="todo-item">
      <input id={todo.id} type="checkbox" />
      <label for={todo.id} class="tick"></label>
      <span>{todo.text}</span>
      <button class="delete-todo">
        <svg><use href="#delete-icon"></use></svg>
      </button>
    </li>
  {/each}
</ul>

What this means is that, for each todo in todoItems, the li element is appended to the DOM.If you’re coming from React or Vue, you know that you need to add a key to each item when rendering a list. In Svelte, you can do this using the (key) syntax in the each constuct. This helps Svelte figure out what items are changed, added or removed in an efficient manner. In this instance, I’m using each todo’s id as the key.

Try it out. Each new todo item added to the list will now be rendered on the page.

Mark todo items as completed

Next, we need a way to indicate that a todo item has been completed. That’s what the checked property on the todo object is for. When a todo item is done, this property needs to be toggled to true and vice versa. Add a new toggleDone function below addTodo:

function toggleDone(id) {
  const index = todoItems.findIndex(item => item.id === Number(id));
  todoItems[index].checked = !todoItems[index].checked;
}

Then update the li element in the each block as follows:

<ul class="todo-list">
  {#each todoItems as todo (todo.id)}
    <li class="todo-item {todo.checked ? 'done' : ''}">
      <input id={todo.id} type="checkbox" />
      <label for={todo.id} class="tick" on:click={() => toggleDone(todo.id)}></label>
      <span>{todo.text}</span>
      <button class="delete-todo">
        <svg><use href="#delete-icon"></use></svg>
      </button>
    </li>
  {/each}
</ul>

Note that, as shown above, you need to place your handler in an arrow function when you want to pass arguments to the function. If you do on:click={toggleDone} instead, the DOM event is what will be passed into the function.

Once the checked property on a todo is set to true, the done class is toggled on the li element. This has the effect of showing a checkmark in the checkbox of the item and striking out the text.

Delete a todo item

Removing an item from the list is easy enough. Add a new deleteTodo function below toggleDone:

function deleteTodo(id) {
  todoItems = todoItems.filter(item => item.id !== Number(id));
}

Then bind it to the button inside the li element:

<button class="delete-todo" on:click={() => deleteTodo(todo.id)}>
  <svg><use href="#delete-icon"></use></svg>
</button>

That’s it! Clicking the x button will now remove the item from todoItems while Svelte takes care of the DOM update.

Keep focus on the form input

At the moment, once a new todo item is submitted the input goes out of focus and has to be refocused manually each time you want to add another todo. This can become quite tedious if you want to add a large amount of items. To keep focus on the input, we can hook into the afterUpdate lifecycle hook which runs after the DOM is updated.

Import afterUpdate at the top of the <script> tag in App.svelte and use it as follows:

<script>
 import { afterUpdate } from 'svelte';

 afterUpdate(() => {
   document.querySelector('.js-todo-input').focus();
 });

 // rest of the code
</script>

That solves the problem quite nicely.

Conclusion

That concludes my tutorial. I hope it has helped you learn the basics of Svelte and how you can use it to build user interfaces. Feel free to reach out in the comments if you got stuck, or if something was not clear enough.

The complete code for this tutorial can be found in this GitHub repository. If you’re interested in learning more about Svelte, consider subscribing to my email newsletter so you do not miss future tutorials.