Build a CRUD Todo App with Vanilla JavaScript and Fetch API

Published on Thursday, 11 January 2024

In the previous article, we discussed setting up the front end for a Golang Todo App. In this post, we will focus on building the frontend portion of a Todo App using Vanilla JavaScript and Fetch API.

This tutorial will guide you on how to create a Todo app frontend with Vanilla JavaScript and Fetch API. We will perform CRUD operations, including;

  • creating a Todo,
  • reading a list of Todo items,
  • editing a Todo item from the list of displayed items,
  • deleting an item from the list,
  • and marking a Todo item as completed or not.

For those who prefer to work with TypeScript, I will create a TypeScript version that provides type-checking and safety toolings, preventing errors during data interaction and making the code less prone to errors. However, Vanilla JavaScript offers simplicity and a flexible experience, making it perfect for learning the fundamentals of web development.

Regardless of the technology you choose, the core concepts remain the same, and we will perform the same CRUD operations mentioned above and utilize the same technologies..

This post is perfect for anyone interested in performing CRUD operations using Vanilla JavaScript and Fetch API, even without setting up a Go server. We will use HTML, CSS, TypeScript, and JavaScript for the front-end development.

You can find the TypeScript version of this article here.

What we will be building

Project Setup

For instructions on setting up HTML templates and static assets (CSS, TypeScript) for a Go web server, refer to this article.

We will write the JavaScript code inside the script tag in the HTML template. However, you can write yours in a separate script.js file and import it into the HTML file if that works better for you.

If you are new to coding, I'd advise you to follow the instructions and type the code yourself.

HTML and CSS Boilerplate

You can download the HTML and CSS files from this GitHub repository, If you wish to use these files’ content.

Building the Frontend CRUD Functionality

Note: localhost:9000/todo is from the local server we created in the Todo Golang Backend in the previous article. Feel free to work with any API endpoint of your choice.

Get and Display a list of todo items

Make a Get Request with the Fetch API

To make a GET request to the API endpoint, we create a getTodos() function. This function is responsible for making the HTTP GET request to localhost:9000/todo and fetching a list of all the todos from the specified API endpoint.

/html/index.html

1<script>
2    const localhostAddress = "http://localhost:9000/todo";
3
4  async function getTodos() {
5        try {
6          const response = await fetch(localhostAddress);
7          const responseData = await response.json();
8          return responseData.data;
9
10        } catch (error) {
11          console.error("Error:", error);
12        }
13      }
14</script

In the code above, we save the API endpoint in a string called localhostAddress so it can be easily reused.

The getTodos() function uses the Fetch API to send the request, await the response, and then parses the response data as JSON. We return the data object in the responseData variable because that is the server response data object we need.

To confirm that you get the response object from the fetch request, you can console.log the responseData and call the getTodos() function. Don’t forget to restart your go server if using one.

get-todos-fetch-api-call.jpg

NOTE: The way we access the todo items in the response (responseData.data) is specific to this example API. If you're using a different API, the response data might be structured differently.

Displaying Todos in the DOM

Now that we have created an asynchronous function for fetching all the todos, let's use the getTodos() function to fetch the list of todos from the API and display the items on the DOM.

If you check the HTML structure, there’s an empty div element with ID todos. This element will serve as the container for dynamically displaying our todo items. The goal is to dynamically update this section based on the items retrieved from the API call.

To do this, we create an asynchronous function called displayTodos. This function will be responsible for dynamically rendering the todo items. Place the function below getTodos() .

1async function displayTodos() {
2  const todoList = await getTodos();
3  let todoListContainer = document.querySelector("#todos");
4
5  if (todoList.length == 0) {
6    todoListContainer.innerHTML += `
7            <div class="todo">
8                <span> You do not have any tasks </span>
9            </div>
10            `;
11  } else {
12    todoList.forEach((todo) => {
13      todoListContainer.innerHTML += `
14        <div class="todo">
15            <span>${todo.title}</span>
16
17            <div class="actions">
18                <button class="edit">
19                    <i class="fas fa-edit"></i>
20                </button>
21                 <button class="delete">
22                <i class="far fa-trash-alt"></i>
23                </button>
24            <div>
25            
26        </div>
27        `;
28    });
29  }
30}
31displayTodos();
32

In the code above, we call getTodos() to fetch a list of todo items and await the result.

We use the document.querySelector method to find the HTML element with ID todos and store it in the todoListContainer variable.

Next, we check if the todoList is empty. If it is, we will update todoListContainer.innerHTML with a message informing the user that there are no tasks created yet.

If the todoList is not empty, we loop through the todoList to access each todo item.We generate an HTML structure for each todo item including the todo title and action buttons for editing and deleting a todo.

We call the displayTodos() function to ensure that the list is displayed when the App loads. Depending on what is returned from your server, the DOM will update the screen with the HTML Todo structure list accordingly.

display-todo-ui.jpg

Creating a Todo

Make a Post Request with Fetch API

To add a Todo task, we must first create a function to make a POST request to the API for adding a Todo to the database.

We will create a function called createTodo to make this HTTP post request to the API endpoint using fetch API and with the user input(todo task ) as part of the request body because the API is expecting it.

Add this code below getTodos and before displayTodos.

1async function getTodos() {
2  // code implementation
3}
4
5async function createTodo(data) {
6  try {
7    // send POST request with user input as the req body
8    const response = await fetch(localhostAddress, {
9      method: "POST",
10      headers: {
11        "Content-Type": "application/json",
12      },
13      body: JSON.stringify(data),
14    });
15
16    const result = await response.json();
17    console.log("Success:", result.message);
18  } catch (error) {
19    console.error("Error:", error);
20  }
21}
22
23async function displayTodos() {
24  // code implementation
25}
26

In the code above, we defined a function called createTodo. It uses the javascript fetch API to send a post request to the localhostaddress.

The createTodo function takes a data argument which is which is sent as part of the request body and converts it to a JSON string.

We set the content type of the request header to "application/json" to indicate that the response body object will be in JSON format.

To prevent the app from crashing due to an error, the code is wrapped in a try-catch block. If the response is successful, a success message is logged to the console.

Create Todos From the User Interface

To allow users to add a new task from the app's user interface, we have an input box and a submit button.

1const localhostAddress = "http://localhost:9000/todo";
2const newTodoInput = document.querySelector("#new-todo input");
3let submitButton = document.querySelector("#submit");
4

We use the query selector to get the input element and save it in a constant called "newtodoinput". Similarly, we save the submit button in a constant called "submitButton".

Next, we will define an asynchronous function called addTasks that will be responsible for adding a new task to the user interface when the user clicks the submit button.

Add the following code below the createTodo function.

1async function createTodo() {
2  // code implementation
3}
4
5async function addTask() {
6  const data = { title: newTodoInput.value };
7  await createTodo(data);
8  displayTodos();
9  newTodoInput.value = "";
10}
11
12async function displayTodos() {
13  const todoList = await getTodos();
14  let todoListContainer = document.querySelector("#todos");
15  // rest of code implementation
16}
17displayTodos();
18
19submitButton.addEventListener("click", () => addTask());
20

When a user clicks the submit button, the addtask function is called because we attached a click event listener to the submit button.

Inside the function, we create a data object with a title key and set its value to the user's input value.

We then call the createTodo function and pass the data object as a parameter to send a POST request to the server, sending the new task data to the backend.

We await the result and call displayTodos to refresh the DOM and display the newly created todo task if the request was successful. Finally, we clear the input field by setting newtodoinput.value to an empty string.

Remember to only access newtodoinput.value when you want to use it. Avoid adding .value at the end of the newtodoinput constant, as it will result in an empty input value.

Lastly, attach an event listener to the submitButton to call the addTasks function when clicked.

Update displayTodo function to fix duplication on the DOM

If you try adding a Task, you will notice the values are duplicated on the DOM.

To avoid duplicated values on the DOM when adding a task, clear the todoListContainer element before populating it with new data. To do this, add todoListContainer.innerHTML = ""; to the displayTodos() function.

1async function createTodo() {
2  // code implementation
3}
4
5async function addTask() {
6  const data = { title: newTodoInput.value };
7  await createTodo(data);
8  displayTodos();
9  newTodoInput.value = "";
10}
11
12async function displayTodos() {
13  const todoList = await getTodos();
14  let todoListContainer = document.querySelector("#todos");
15  todoListContainer.innerHTML = "";
16  // rest of code implementation
17}
18displayTodos();
19
20submitButton.addEventListener("click", () => addTask());
21

With these additions, users can now easily add tasks from the app's user interface.

To view changes, compile the file tsc static/script.ts and restart the Go server if using one.

Deleting a Todo

Delete Todo with Fetch API and Vanilla JavaScript

When you want to delete a task from the user interface, you need to send a DELETE request to the API endpoint. To create this functionality, we define a “deleteTodo” asynchronous function. This function will be responsible for sending a HTTP DELETE request to the endpoint URL, along with the ID of the task you want to delete.

Place the deleteTodo function after the createTodo function but before the Addtask function.

1async function createTodo() {
2  // code implementation
3}
4
5async function deleteTodo(TodoID) {
6  try {
7    const response = await fetch(`${localhostAddress}/${TodoID}`, {
8      method: "DELETE",
9    });
10    const result = await response.json();
11    console.log("Success:", result.message);
12  } catch (error) {
13    console.error("Error:", error);
14  }
15}
16
17async function addTask() {
18  // code implementation
19}
20

The deleteTodo function takes in a parameter TodoID, which is the ID of the tasks you want to delete.

To catch any errors that occur and log them gracefully to the console, we place this function call in a try-catch block.

We use the Fetch API to send an HTTP DELETE request to localhostAdress with the ID in the URL. We then await the response from the request and parse it as JSON.

If the request was successful, we console log the success message returned from the server. In this case, we do not return the result because we don’t need it.

Delete Todo Item from the User Interface

To delete a Todo item from the user interface, we need to manipulate the DOM.

As a quick recap, When we created the todoContainer.innerHTML to display the list of Todos, we also created an action button for deleting each Todo task. We will now delve deeper into this.

To add functionality to the delete button action, we define a deleteTaskButton function below displayTodos() function.

1function deleteTaskButton() {
2  const deleteTodoButtons = document.querySelectorAll(".delete");
3
4  for (const deleteButton of deleteTodoButtons) {
5
6    deleteButton.onclick = async function () {
7      const todoID = deleteButton.getAttribute("data-id");
8      await deleteTodo(todoID);
9      displayTodos();
10    };
11  }
12}
13

Inside the function, we use the queryselectorAll to select all the elements on the DOM with the class name "delete". We then loop through the deleteTodoButtons node list and assign an onlick evenlister to each button.

When a user clicks the delete icon button:

We create a constant todoID that we will use to access the ID for each todo item. To get the TodoID, we update the button element with a data-id attribute.

If you check the code snippet below, you will see I updated the button element with class ’delete’ to show how to add the data-id attribute.

1async function displayTodos() {
2
3  <button data-id=${todo.id} class="delete">
4  // rest of code...
5}
6

Next, we assign the todo.id as the data-id attribute value. which is one of the properties of a todo in the **todolist**.

To get the todo.id saved in the data-id attribute, we use the getAttribute method and store the value in a todoID.

Next, we call the deleteTodo function and pass in the todoID as a parameter to delete the item and await the result. After the item is deleted, we refresh the todo list to show the updated list.

Finally, we call the deleteTaskButton() function inside displayTodos() so that it is available to displayTodos when a user clicks the delete button.

1async function displayTodos() {
2  // code implementation ...
3
4  deleteTaskButton();
5}
6displayTodos();
7

To view changes, compile the file tsc static/script.ts and restart the Go server if using one.

Update a Todo

To update a To-do item, we will be implementing two forms of editing.

  1. Users will be able to edit the To-do item title.
  2. Users will be able to mark a To-do item as completed or not.

Users will be able to edit the To-do item title.

To edit the To-do item title, we will start by defining an asynchronous function that will make an API call to update the To-do item in the database. This function will take a TodoID and the data object to be passed as the parameters and part of the request body.

Place the code below deletTodo and addTask function

1async function deletTodo() {
2  // code implementation
3}
4
5async function updateTodo(id, data) {
6  try {
7    const response = await fetch(`${localhostAddress}/${id}`, {
8      method: "PUT",
9      headers: {
10        "Content-Type": "application/json",
11      },
12      body: JSON.stringify(data),
13    });
14    const result = await response.json();
15    console.log("Success:", result);
16  } catch (error) {
17    console.error("Error:", error);
18  }
19}
20
21async function addTask() {
22  // code implementation
23}
24

After creating the updateTodo async function for updating a To-do Item HTTP Request, When a user clicks the edit button, we want to load the To-do item title into the input box. To achieve this, we need to create a variable to track whether the user is currently editing a To-do item or not. This is necessary because we are using the same input box for both editing and adding new items to the list.

At the beginning of the file where you defined your variable, create isEditingTask and set it to false. Also create an editButtonTodoID variable, we will use it to store the id of the todo we want to edit.

1const submitButton = document.querySelector("#submit");
2
3let isEditingTask = false;
4let editButtonTodoID = '';
5

Next, navigate to the displayTodos function and add the data-id attribute to <button class=edit>. Also, add a editTaskTitleButton below deleteTaskButton in the displayTodos function.

1async function displayTodos() {
2  <button data-id=${todo.id} class="edit">
3  // code that lists the todo...
4
5  deleteTaskButton();
6  editTaskTitleButton();
7}
8

Now, let’s define a editTaskTitleButton function. Place this code below deletTaskButton()

1function deleteTaskButton() {
2	// ...
3}
4
5function editTaskTitleButton() {
6  const editTodoTitleButtons = document.querySelectorAll(".edit");
7
8  for (const editButton of editTodoTitleButtons) {
9    const todoName = editButton.parentNode.parentNode.children[0];
10
11    editButton.onclick = async function () {
12      newTodoInput.value = todoName.innerText;
13      submitButton.innerHTML = "Edit";
14      isEditingTask = true;
15
16      editButtonTodoID = editButton.getAttribute("data-id");
17    };
18  }
19}
20

In the code above, we use the document.querySelectorAll to fetch all the button elements with class='edit'. we save the NodeList element in a variable called editTodoTitleButtons. Next, we loop through the list to access the to-do button.

To access the todo item title, we can use the editButton.parentNode.parentNode.children[0] code. This code moves up the DOM hierarchy, allowing us to select the first child element of the second parent. We store this element in the todoName variable, giving us access to the inner text representing the title of the todo item.

Now that we can access the todo item title in the DOM, we want to load the text into the input box. To do that, we attach an onclick event listener to the “edit” button so when a user clicks on the edit button, we:

  • we set the input box value to the todo item title text
  • change the submit button text to edit (this is optional)
  • set isEditingTask variable to true
  • get the todo ID from the data-id attribute and save it in the editButtonTodoID variable we defined at the top.

Your app should be able to load a todo title into the input box when you click on it now.

Next, let's create an edit tasks function to handle editing a todo item on the UI. For now, we can only load the text into the input field. Place the code below addTask() function.

1async function addTask() {
2  // code implementation
3}
4
5async function editTask() {
6  const data = { title: newTodoInput.value, completed: false };
7
8  if (isEditingTask) await updateTodo(editButtonTodoID, data);
9  displayTodos();
10
11  newTodoInput.value = "";
12  isEditingTask = false;
13  submitButton.innerHTML = "Add";
14}
15
16async function displayTodos() {
17  // code implementation
18}
19

In the code above, we create a data object with the title and completed property. The title is set to newTodoInput.value, which is whatever is in the input box when the user clicks the submit button. We set completed to false for now because the API expects a completed property to be present; later, we will update the value for the completed prop accordingly.

Next, we check to see if isEditingTask is true, which means the user is editing a task, remember we set isEditingTask to true when a user clicks the edit button. We call the updateTodo function and pass in the TodoID and data object as arguments. We await the result and refresh the to-do item list when done. Next, clear the input field and set isEditingTask to false to indicate we are done. Finally, update the submit button text back to "Add".

Lastly, update the submit button event listener to call editTask function if isEditingTask is true.

1submitButton.addEventListener('click', () => isEditingTask ? editTask() : addTask())
2

To view changes, restart the Go server if using one.

Users will be able to mark a To-do item as completed.

To implement this functionality, the process involves two main steps:

  1. Styling the to-do title text to indicate whether an item is marked as completed.
  2. Updating the completed property within the data object to reflect the item's status on the server

Styling the to-do title text to indicate whether an item is marked as completed

First, we want to style the todo title text to visually indicate whether an item is marked as completed or not. Update the HTML structure within the displayTodos function:

1<div class="todo">
2  <span
3    id="todoname"
4    style="text-decoration:${todo.completed ? 'line-through' : ''}"
5    data-iscomplete="${todo.completed}"
6    data-id="${todo.id}"
7  >
8    ${todo.title}
9  </span>
10   ...
11</div

In the code above:

  • We use the style attribute to set the text-decoration property of the todo title span element.
  • If todo.completed is true, we set text-decoration: line-through to visually strike through the text, indicating that the item is completed.
  • If todo.completed is false, no text-decoration is applied.

We also add two custom attributes, data-iscomplete and data-id, to store the completed status and the ID for each todo item. These attributes will help us access and update the values later.

We also added an HTML id attribute to make it easy to fetch the list of all the to-do titles displayed on the DOM.

Toggle Todo Item as Completed or Not

Next, we want to allow users to click on a todo item to toggle its completion status. To achieve this, add the following code. below editTaskTitleButton() function.

Also call the toggleTaskCompletion inside the dislayTodos function like we did for deleteTaskButton and editTaskTiltleButton

1async function displayTodos() {
2  <button data-id=${todo.id} class="edit">
3  // code that lists the todo...
4
5	deleteTaskButton();
6	editTaskTitleButton();
7	toggleTaskCompletion();
8}
9
10function deleteTaskButton() {
11  // code implementation
12}
13
14function editTaskTitleButton() {
15  // code implementation
16}
17
18function toggleTaskCompletion() {
19  const editTaskCompleted = document.querySelectorAll("#todoname");
20
21  for (const task of editTaskCompleted) {
22    task.onclick = async function () {
23      const isTaskDone = JSON.parse(task.getAttribute("data-iscomplete"));
24      const todoID = task.getAttribute("data-id");
25
26      const data = { title: task.innerText, completed: !isTaskDone };
27      await updateTodo(todoID, data);
28      displayTodos();
29    };
30  }
31}
32

In the code:

  • we fetch all HTML elements with ID #todoname and store them in a todoTitleEmlement variable.
  • We iterate through the list of HTML elements to get access to the data-iscomplete and data-id attribute for each element.

When a user clicks on a todo item title, the following actions occur:

  • We use JSON.parse to convert the data-iscomplete attribute from a string to a boolean value.

  • We use the getAttribute method to fetch the data-iscomplete and data-id values and store them in isTaskDone and todoID variable.

  • We create a new data object with the title property set to the text of the clicked todo item text.

  • we set completed property to !istaskDone to toggle the value of the completed status between true and false.

  • We call the updateTodo function with the ID and data object as arguments to update the completed status and await the result.

  • Finally, we call displayTodos to refresh the todo list and display the changes.

This completes the functionality of marking a Todo item as completed or not and updates the UI accordingly.

Your App should look like this;

Update the completed property when editing the To-do item title.

In our previous work, specifically within the editTask function, we focused on updating the todo title. At that time, we hardcoded the completed property to false.

Now, we aim to improve the code by dynamically setting the completed property to the value stored in the data-iscomplete property.

Here are the steps:

  1. Create an isComplete Variable

To store the value from the data-iscomplete property, we create an isComplete variable and set it to false:

1let isEditingTask = false;
2let todoID = ''
3let isComplete = false
4
  1. Update the editButton.onclick function inside editTaskTitleButton()

Within the editButton.onclick function, we need to set the isComplete variable to the value of the data-iscomplete property. This is done when a user clicks on the edit button:

1editButton.onclick = async function () {
2  newTodoInput.value = todoName.innerText;
3  submitButton.innerHTML = "Edit";
4  isEditingTask = true;
5
6  editButtonTodoID = editButton.getAttribute("data-id");
7  isComplete = JSON.parse(todoName.getAttribute("data-iscomplete"));
8};
9

Now, when a user clicks the edit button, it updates the isComplete variable with the value from the data-iscomplete attribute. JSON.Parse converts it from a string to a boolean.

Lastly, In the editTask function, where we set up the data object to send to the server, update the completed property of the data object with the value of the isComplete variable.

1async function editTask() {
2  const data = { title: newTodoInput.value, completed: isComplete };
3
4  if (isEditingTask) await updateTodo(editButtonTodoID, data);
5  displayTodos();
6
7  newTodoInput.value = "";
8  isEditingTask = false;
9  submitButton.innerHTML = "Add";
10}
11

This ensures that when you edit a todo item, the completed property retains its previous value, either true or false based on data-iscomplete attribute.

With these changes, your code effectively handles marking todo items as done and updating the completed attribute when editing a todo item.

View the complete code on GitHub

Conclusion

In this tutorial, we've built a Todo app's frontend using Vanilla JavaScript and the Fetch API. We've covered all the CRUD operations:

  1. Fetched and displayed todo items from the API.
  2. Enabled users to add new tasks and updated the UI accordingly.
  3. Implemented the ability to delete tasks from the UI.
  4. Enhanced the editing feature, allowing both title updates and marking tasks as completed.
  5. Styled completed tasks by toggling the "text-decoration" CSS property.

Stay tuned for an upcoming TypeScript version of this tutorial.

This guide covers key frontend development skills, including API interaction, CRUD operations, and dynamic user interface creation. Building a Todo app is an ideal starting point for web development, offering insights and skills for broader applications.

Please feel free to email me or drop a comment if you have any questions, feedback, or need help with any part of the tutorial. I hope you found this article helpful.

Recommended Reads

Articles written by Kelechi Ogbonna. All rights reserved

Built with NextJs Typescript and TailwindCSS