Ah yes, the todo list. Probably the most popular thing to build when learning a new programming language. Some people claim building a todo list app is a boring activity since so many exist already.
The todo list app we’ll be building will be pretty basic. A user can add a task, mark a task as completed and delete an already added task. I’ll explain how to build each feature, but you must follow through by typing the code and running it on your end to get the most out of the tutorial.
I recommend using JSFiddle while working through this tutorial, but feel free to use other code playgrounds or your local text editor if you prefer.
Without further ado, grab the markup and styles for this project on JSFiddle. If you’re using JSFiddle, hit the the Fork button to create a new fiddle of your own.
Add a todo
The first thing we need to do is set up an array to hold our todo list items. Each todo will be an object with three properties:
text which holds whatever the user types into the text input,
checked which helps us know if a task has been marked completed or not, and
id, a unique identifier for the todo item.
Once the user adds a task, we’ll push a new todo object into the array and render the
text on the screen. When the user completes a todo by checking it off, we’ll toggle the
checked property to
true, and when the user deletes a todo, we’ll locate the todo item in the array using its
id and remove it.
Let’s start by adding a todo item to our list. To do so, we need to listen for the
submit event on
.js-form, and then invoke a new
addTodo() function when the form is submitted.
Normally, when a form is submitted, the browser will attempt to submit the form to a server which will cause a page refresh. Since we’re not interested in doing that, we stop the default behaviour using
Next, we select the text input and trim its value to remove whitespace from the beginning and end of the string, and save it in a new variable called
text variable is not equal to an empty string, we pass the text to the
addTodo() function which has been defined above the event listener. Within the function, we create a new object for the task and add the properties I mentioned earlier.
text property is set to the text from the input,
checked is initialised to
id is initialised to the number of milliseconds elasped since January 1, 1970. This
id will be unique for each todo item, unless you can add more than one task in a millisecond, which I don’t think is possible.
Finally, the task is pushed to the
todoItems array, and the array is logged to the console. The value of the text input is also cleared by setting it to an empty string, and is also focused so that the user can add multiple items to the list without having to focus the input over and over again.
Add a few todo items and view the
todoItems array in the console. You will see each todo item represented by an object in the array.
Render the todo items
Once we add a todo to the
todoItems array, we want the page to be updated and the item rendered on the screen. We can do this pretty easily by appending a new
li element for each item to
To achieve this, replace the
console.log() statement in
addTodo() as follows:
insertAdjacentHTML() method allows us to add some HTML to the DOM using an already present element as a reference point. In this case, we’re using the
.js-todo-list as a reference and appending a new
li element for each todo item before its closing tag.
Notice that on each todo item, we have a
data-key attribute that is set to the item’s
id. This is an important step, as it allows us to locate a todo item easily in the DOM. You’ll see how we’ll use it when we get to the section on checking off and deleting tasks.
Try it out. When a new task is entered by the user, it will be rendered on the page.
Mark a task as ‘done’
The next feature to implement is the ability to mark a task as completed. To do so, we need to listen for the click event on the checkbox and then toggle the
checked property on the todo item as needed.
Add the following event listener at the bottom of the JS pane:
Instead of listening for clicks on individual checkbox elements, we are listening for clicks on
.js-todo-list. When a click event occurs on the list, we check if the element that was clicked is the checkbox. If so, we extract the value of
data-key on the checkbox’s parent element, and pass it off to a new
toggleDone() method which will be created below the
This function receives the key of the list item that was checked or unchecked and finds the corresponding entry in the
todoItems array using the
findIndex method. Then we toggle the value of the checked property to the opposite value. Finally, the
done class is added or removed from the item depending on its
checked status. This class has the effect of striking out the text and showing a checkmark in the checkboxes of completed items.
Delete todo items
Similar to the way we implemented the last feature, we’ll listen for clicks on the
.js-delete-todo element, then grab the key of the parent and pass it off to a new
deleteTodo function which will remove the corresponding entry in
todoItems and remove the todo item from the DOM.
First, detect when the delete button is clicked:
Then create the
deleteTodo() function below
toggleDone() as follows:
Now, you should be able to delete tasks by hitting the delete button.
Add an empty state prompt
An empty state occurs when there is no data to show in the app. For example, when the user hasn’t added a todo yet (first use) or when the user has cleared the list. It is important to account for this state when designing an application.
Many apps use the empty state to show a prompt that tell the user what to do. Here is a real-world example of what a good empty state prompt looks like:
And here’s an example of a not so great empty state design:
For this exercise, once there are no tasks to display, we’ll add a prompt that encourages the user to add a new task. This feature can be implemented with just some HTML and CSS. We will take advantage of the
:empty selector in CSS to display the prompt conditionally only if no items exist in the list.
Add the markup for the empty state prompt as shown below:
Then add some styles for the empty state in your CSS:
While this looks just fine, the problem is the message persists even when a task has been added to the list. The intended behaviour is for the prompt to disappear once a todo has been added and only reappear when there are no more tasks to display.
This bit of CSS will give us what we want:
.empty-state is hidden from view by default with
display: none and only comes into view when
.todo-list is empty. We’re using the :empty selector to detect when
.todo-list is empty, and the sibling selector (
+) to target
.empty-state and apply
display: flex to it only when
.todo-list is empty.
Now, we have a fully functional todo list application, and we didn’t have to write a lot of code to achieve this.
A subtle bug
One issue I encountered while working on this tutorial is that the empty state wouldn’t return into view when all existing tasks are deleted.
The problem is that some whitespace persists in the
.todo-list element even though all the child
deleteTodo() function as follows:
The above code solves the problem, and the app now works as expected.
In this article, I showed you how to build a simple todo list app that allows the user to add new tasks, mark a task as completed and delete old tasks. I also discussed the importance of accounting for empty states when designing an application, then proceeded to talk about a potential problem when using the
:empty selector and how to fix it.
I hope you learnt something. If a section or piece of code is not clear to you, feel free to leave a comment below and I’ll get back to you as soon as possible.
Thanks for reading!