DEV Community

Cover image for Learn Svelte: Adding, Editing and Estimating Tasks In The Pomodoro Technique App
Jaime González García
Jaime González García

Posted on • Edited on • Originally published at barbarianmeetscoding.com

Learn Svelte: Adding, Editing and Estimating Tasks In The Pomodoro Technique App

This article was originally published on Barbarian Meets Coding.

Svelte is a modern web framework that takes a novel approach to building web applications by moving the bulk of its work from runtime to compile-time. Being a compiler-first framework allows Svelte to do some very interesting stuff that is unavailable to other frameworks like disappearing from your application at runtime, or allowing for a component centered development with HTML, JavaScript and CSS coexisting within the same Svelte file in a very web standards friendly fashion.

In this series we'll follow along as I use Svelte for the first time to build an app. I'll use my go-to project[^1] to learn new frameworks: A Pomodoro Technique app, which is a little bit more involved than a TODO list in that it has at least a couple of components (a timer and a list of tasks) that need to interact with each other.

In this part 3 of the series we continue our project by making it possible to create a list of tasks and estimate the number of pomodoros it will take to perform them. Let's get started!

Haven't read the other articles in this series? Then you may want to take a look at this list of resources for getting started with Svelte or the first part of building the Pomodoro Technique app

Starting a Daily Pomodoro Routine

When you follow the Pomodoro Technique, the first thing that you'll do everyday before you start working is to sit down and follow these steps:

  1. Decide what tasks you want to achieve today,
  2. Estimate how many pomodoros it will take to fulfill them and, then
  3. Prioritize them taking into account how many pomodoros you can realistically achieve

Let's improve our skeletal Pomodoro app to support this initial flow by providing a way to create and estimate Tasks.

Defining a Way to Model a Task

The first thing that we need to do is to devise a way to model a task. In our current version of the app, a task is just a string that represents a description of whatever we need to get done:

<script>
  const tasks = [
    "plan some fun trip with Teo",
    "buy some flowers to my wife",
    "write an article about Svelte"
  ];
</script>

<style>
  ul {
    list-style: none;
  }
</style>

<ul>
  {#each tasks as task}
    <li>{task}</li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

But we need our tasks to be slightly more involved with information such as the number of pomodoros we expect the task will take, the status of the task (is it completed or not?) and the actual number of pomodoros the task took.

So we'll model the task using a class Task within a new file Task.js with some initial fields to cover our initial use case:

export class Task {
  constructor(description="", expectedPomodoros=1) {
    this.description = description;
    this.expectedPomodoros = expectedPomodoros;
    this.actualPomodoros = 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now replace the strings from our original example with instances of this class:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];
</script>
Enter fullscreen mode Exit fullscreen mode

The UI remains the same for now. We've just changed the underlying way in which we represent a task. Now, let's make it possible to add new tasks.

Creating new Tasks

Our goal for this tutorial will be to get to a working implementation of a Pomodoro technique app as soon as possible, so we'll focus on getting there fast with little regard for an amazing user experience or great design. We will center our development in getting the basic core functionality in place and later we will polish and refine.

In order to have a quick implementation of an editable list of tasks where one can add and remove tasks to their heart's content we're going to follow this approach. We'll:

  1. Make all tasks editable by using inputs for each task
  2. Add a button to add new tasks
  3. Add a button to remove tasks beside each one of the tasks

Making Tasks Editable

In order to make our tasks editable we are going to update our TaskList.svelte component. Instead of plain list elements:

<ul>
  {#each tasks as task}
    <li>{task}</li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

We'll use inputs:

<ul>
  {#each tasks as task}
    <li>
      <input type="text" value={task.description}>
      <input type="number" value={task.expectedPomodoros}>
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

The example above seems like it's working but it really isn't. The value={task.description} only works one way, from the data into the template. But if a user tries to edit a task, the new description or pomodoros won't be reflected in the data. The way to establish a two-way data binding between data and template is by using the bind:value directive:

<ul>
  {#each tasks as task}
    <li>
      <input type="text" bind:value={task.description}>
      <input type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

And now we can edit task descriptions and the number of pomodoros we expect each task will take. Whenever we update the underlying data, it will be reflected in the inputs, and likewise, whenever we update the inputs, the changes we do will be reflected in the data.

A big Title 'IL POMODORO' followed by a list of editable tasks within input boxes

Let's adjust the styles a little bit so that the input fields suit their content better:

<style>
  ul {
    list-style: none;
  }
  .description {
    min-width: 400px;
  }
  .pomodoros { 
    max-width: 100px;
  }
</style>

<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description}>
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

Svelte styles are scoped to the component itself so I could've styled the input elements directly (with nth-child selectors for instance), but I like to use semantic named classes for two reasons:

  • They are easier to read and make sense of
  • If I happen to change the order of the inputs at some point in the future I won't break the styles of my app

Now it looks better! Nice!

A big Title 'IL POMODORO' followed by a list of editable tasks within input boxes

Adding New Tasks

The next thing we want to do is to be able to add new tasks. So we add a button that will perform that function:

<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description} >
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
  <button>Add a new task</button>
</ul>

Enter fullscreen mode Exit fullscreen mode

Whenever we click on this button we will add a task to the list of tasks that we want to complete today. In order to do that, we handle the click event using the on:{event} directive so that every time a user clicks on that button a new task is created and added to our list:

<button on:click={addTask}>Add a new task</button>
Enter fullscreen mode Exit fullscreen mode

The addTask function belongs to the behavior-y portion of our Svelte component inside the script tag:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks.push(new Task());
  }
</script>
Enter fullscreen mode Exit fullscreen mode

And now when I click on the button to add a new task... nothing happens. Hmm...

After some tinkering and troubleshooting I realized that the way Svelte knows whether something changes is through a new assignment. So we need to update our code above to the following:

function addTask(){
  tasks = tasks.concat(new Task());
}
Enter fullscreen mode Exit fullscreen mode

I also learnt some interesting things:

  • Svelte has nice support for sourcemaps so I can look at Svelte code in Chrome Dev Tools. However, I cannot set a break point or use logpoints inside the addTask method.
  • With the aid of console.log inside addTask and the {@debug tasks} on the Svelte template I could see how the list kept growing but the template was never updated. After fixing the issue, as the list kept growing the {@debug tasks} was executed and logged the updated list of tasks.
<script>
  import {Task} from './Task.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks.push(new Task());
    console.log(tasks); // => this grows everytime
  }
</script>

<!-- this was only executed the first time -->
{@debug tasks}
<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description} >
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
    </li>
  {/each}
  <button on:click={addTask}>Add a new task</button>
</ul>
Enter fullscreen mode Exit fullscreen mode
  • It is very easy to inspect the generated code both within the Svelte Playground or when developing Svelte locally. The output JavaScript produced for pushing a task in the existing array is:
function addTask() {
  tasks.push(new Task());
}
Enter fullscreen mode Exit fullscreen mode

Whereas if we update the value of the tasks variable the following code is generated:

function addTask() {
  $$invalidate(1, tasks = tasks.concat(new Task()));
}
Enter fullscreen mode Exit fullscreen mode

That $$invalidate function must be the one that warns Svelte that data has changed and that the template (the part that depends on tasks) needs to be re-rendered.

Anyhow! Now we can add new tasks:

A big Title 'IL POMODORO' followed by a list of editable tasks within input boxes. There's a button to add new tasks.

Removing Existing Tasks

We can add tasks, so we should also be able to remove tasks whenever we change our priorities. In order to do that, we add a new button for each task:

<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description}>
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
      <!-- NEW STUFF -->
      <button on:click={() => removeTask(task)}>X</button>
      <!-- END NEW STUFF -->
    </li>
  {/each}
  <button on:click={addTask}>Add a new task</button>
</ul>
Enter fullscreen mode Exit fullscreen mode

And create a new removeTask method to perform the actual removing:

function removeTask(task){
  const index = tasks.indexOf(task);
  tasks = [...tasks.slice(0, index), ...tasks.slice(index+1)];
}
Enter fullscreen mode Exit fullscreen mode

JavaScript should really have an array.prototype.remove method... FizzBuzz it, let's do it (one should never, ever do this at home or work. Only on hobby projects with zero stakes).

I add a new ArrayExtensions.js file with this beautiful thing:

/**
 * Returns a new array without the item passed as an argument
 */
Array.prototype.remove = function (item) {
    const index = this.indexOf(item);
    return [...this.slice(0, index), ...this.slice(index+1)];
}
Enter fullscreen mode Exit fullscreen mode

And update our TaskList.svelte component:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks = tasks.concat(new Task());
  }
  function removeTask(task){
    // It looks way nicer, doesn't it?
    tasks = tasks.remove(task);
  }
</script>
Enter fullscreen mode Exit fullscreen mode

And now tasks can be removed:

A big Title 'IL POMODORO' followed by a list of editable tasks within input boxes. There's a button to add new tasks.

A Slightly Better User Experience Using Svelte Lifecycle Hooks

Wouldn't it be nice if the newly created task description would come into focus when a new task is created? That way the keyboard friendly user of our app could press Enter on the Add new task button, type away the task and estimation, press Enter again on the button, and so forth. Maximum productivity.

In order to be able to add this type of functionality we need to know when a new input is added to the DOM, and have that new input get the focus. After taking a quick look at the Svelte docs I found that you can hook into the lifecycle of a component to solve this sort of things. The afterUpdate lifecycle hook is the one that's executed after the DOM has been updated with new data, so that sounds like a good candidate:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';

  // Rest of the code has been collapsed for simplicity's sake

  afterUpdate(() => {
    console.log('Hello! I was updated!'):
  });
</script>
Enter fullscreen mode Exit fullscreen mode

If we take a look at our app right now we'll see how every time the component is rendered we get that message printed in the console. Now we need to get a reference to that input element that gets created. Svelte has a special directive that may help with that bind:this.

You can use it like this:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';
  let lastInput;

  // rest of the code collapsed for simplicity's sake
</script>

<style>
/** styles collapsed **/
</style>


<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description} 
       bind:this={lastInput}>  <!-- THIS IS NEW! -->
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
      <button on:click={() => removeTask(task)}>X</button>
    </li>
  {/each}
  <button on:click={addTask}>Add a new task</button>
</ul>
Enter fullscreen mode Exit fullscreen mode

And now that we have a reference to that input we can use it to make it come into focus when we create a new task:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';
  let taskAddedPendingFocus = false;
  let lastInput;

  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];

  function addTask(){
    tasks = tasks.concat(new Task());
    taskAddedPendingFocus = true;
  }
  function removeTask(task){
    tasks = tasks.remove(task);
  }
  function focusNewTask(){
    if (taskAddedPendingFocus && lastInput) {
      lastInput.focus();
      taskAddedPendingFocus = false;
    }
  }

  afterUpdate(focusNewTask);
</script>
Enter fullscreen mode Exit fullscreen mode

This solution seems quite brittle for several reasons, like the fact that I have an nagging feeling that I can only get away with it because the newly created input is the last input in the DOM. But it will work for now. Sometimes the right solution is the working solution. We shall pay our accrued technical debt soon enough.

For the time being, enjoy with a nice focus behavior:

An animated gif that shows me adding new tasks to a to do list and keeping focus in the description field.

Setting a Goal of Pomodoros for the Day and Keeping it

The final thing that we want to add to support the pomodoro startup flow is to have a way for the user to understand how many pomodoros they're comitting themselves to complete. A quick way to do that is to just sum all the estimated pomodoros for all tasks and show them to the user.

This is a perfect feature because it's simple and it lets us experiment with the reactivity system in Svelte.

In Svelte, you can create properties that are computed from other existing properties. In this case, we need a new property that is the sum all of current pomodoros for all tasks. Such a property could look like this:

<script>
    import { afterUpdate } from 'svelte';
  import {Task} from './Task.js';
  import './ArrayExtensions.js';
  let taskAddedPendingFocus = false;
  let lastInput;
  let tasks = [
    new Task("plan some fun trip with Teo"),
    new Task("buy some flowers to my wife"),
    new Task("write an article about Svelte"),
  ];
  $: allExpectedPomodoros = tasks.reduce((acc , t) => acc + t.expectedPomodoros, 0);

  /** rest of the code omitted for the sake of clarity. **/
</script>
Enter fullscreen mode Exit fullscreen mode

The $: syntax tells Svelte that the allExpectedPomodoros property is a reactive value, and that it needs to be updated any time tasks is updated (The funny thing is that this is actual valid syntax in JavaScript which I've never used in my life).

Now we can add it to our markup:

<ul>
  {#each tasks as task}
    <li>
      <input class="description" type="text" bind:value={task.description} bind:this={lastInput}>
      <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
      <button on:click={() => removeTask(task)}>X</button>
    </li>
  {/each}
  <button on:click={addTask}>Add a new task</button>
</ul>
<!-- New stuff here -->
<p>
  Today you'll complete {allExpectedPomodoros} pomodoros.
</p>
Enter fullscreen mode Exit fullscreen mode

And we are done!

An animated gif that shows me adding new tasks to a to do list and keeping focus in the description field.

And What happens When There's no Tasks?

Ok, just one more thing. There's one last detail that'd be nice to have work out. What happens when there are no tasks?

Right now we just show an empty void of nothingness but it'd be nice to show some encouraging message to our users to have them start the day with strength. Let's do that!

We can take advantage of Svelte's {#if} and {:else} blocks to show a message when there are no tasks yet. For example:

{#if tasks.length === 0}
  <p>You haven't added any tasks yet. You can do it! Add new tasks and start kicking some butt!</p>
{:else}
  <ul>
    {#each tasks as task}
      <li>
        <input class="description" type="text" bind:value={task.description} bind:this={lastInput}>
        <input class="pomodoros" type="number" bind:value={task.expectedPomodoros}>
        <button on:click={() => removeTask(task)}>X</button>
      </li>
    {/each}
  </ul>
{/if}

<button on:click={addTask}>Add a new task</button>

{#if tasks.length != 0}
  <p>
    Today you'll complete {allExpectedPomodoros} pomodoros.
  </p>
{/if}
Enter fullscreen mode Exit fullscreen mode

An inspiring message for when the user hasn't created any tasks yet

Looking for the source code for the pomodoro app?

Look no more! You can find it on GitHub ready to be cloned and enjoyed, or on the Svelte REPL where you can tinker with it right away.

Some More Reflections So Far

In general, working with Svelte has been quite pleasant. Here are some more thoughts to add to the ones from the last article:

  • In general things still work mostly as I expect them to work and it is easy to troubleshoot and recover from errors. I was surprised that array.push didn't trigger a component render but after diving into the issue I understand that it is much easier for the compiler to understand that changes happen on assignments. And it does make a lot of sense, it easier to think of having to update the values of things instead of learning a new API (like setState for instance).
  • It was a pity that I couldn't put breakpoints or logpoints in the svelte code in Chrome Dev Tools. I really expected that to work but perhaps it requires some additional setup that I am unaware of. It does feel like something that should be supported in a dev environment out of the box.
  • It is really cool that the Svelte tutorials and the playground provide access to the code generated by the Svelte compiler. It was fun to take a peek into the generated code and realize that the array.push wasn't generating an invalidating call. (This also shows that Svelte does have a runtime, albeit small, even though people often market it as completely disappearing once your app has been generated).
  • The syntax for handling events, binding elements to data, the if and else blocks, it was non standard but quite reminiscent at times, and in general easy to learn. (Although that may be because of the experience I have with many other frameworks that implement similar capabilities with slightly different syntax)
  • The $: reactive values are really easy to implement and render in your component.

And we have come to an end for today. Hope you enjoyed this article! Take care!

Top comments (0)