We left off in a decent place, but there was some missing functionality that we wanted all along.
- We need to be able to view completed to-dos (and be able to delete them entirely)
- We need to be able edit to-dos.
Article Series
- Building a TODO App from Scratch — Step 1 — Planning & Design
- Building a TODO App from Scratch — Step 2 — HTML
- Building a TODO App from Scratch — Step 3 — Basic JavaScript Functionality
- Building a TODO App from Scratch — Step 4 — Styling & Interactive Choices
- Building a TODO App from Scratch — Step 5 — Extra Functionality
Editing To-Dos
The interaction we decided on for editing is to double-click the todo. This will turn the to-do, in place, into an editable input. Then you hit enter (e.g. submit) or leave the input (e.g. blur) and it will save the changes.
We can use a bit of event delegation to set up this event:
list.addEventListener("dblclick", (event) => {
const listItem = event.target.closest("li");
// If already editing, let it be.
if (listItem.classList.contains("editing")) return;
listItem.classList.add("editing");
// Do editing.
});
Code language: JavaScript (javascript)
Now anywhere you double-click on the list will set the relevant list item into “editing mode”, here indicated by just adding a class we can style against, like hide the existing text.
More importantly, we need to insert some new HTML turning the text into an editable input. We can use a template literal of a <form>
to inject as needed, like so:
list.addEventListener("dblclick", (event) => {
const listItem = event.target.closest("li");
// If already editing, let it be.
if (listItem.classList.contains("editing")) return;
listItem.classList.add("editing");
const textItem = listItem.querySelector(".text");
listItem.insertAdjacentHTML(
"beforeend",
`<form onsubmit="updateTodo(event);" class="form-edit">
<input onblur="updateTodo(event);" type="text" class="input-edit" value="${textItem.textContent}">
</form>`
);
});
Code language: JavaScript (javascript)
That calls an updateTodo()
event we’ll have to write. But first, let’s make sure we focus the input and put the cursor at the end. Just a bit of nice UX right?
list.addEventListener("dblclick", (event) => {
const listItem = event.target.closest("li");
// If already editing, let it be.
if (listItem.classList.contains("editing")) return;
listItem.classList.add("editing");
const textItem = listItem.querySelector(".text");
listItem.insertAdjacentHTML(
"beforeend",
`<form onsubmit="updateTodo(event);" class="form-edit"><input onblur="updateTodo(event);" type="text" class="input-edit" value="${textItem.textContent}"></form>`
);
const input = listItem.querySelector(".input-edit");
input.focus();
// put cursor at end of input
input.setSelectionRange(input.value.length, input.value.length);
});
Code language: JavaScript (javascript)
Updating the to-do is pretty straightforward. We get our hands on the new text, update it in the DOM, toggle the editing class, and write the data back to localStorage. It looks like a lot of lines, but a lot of it is just getting our hands on DOM elements and basic manipulation.
function updateTodo(event) {
event.preventDefault();
const listItem = event.target.closest("li");
const textItem = listItem.querySelector(".text");
const inputItem = listItem.querySelector(".input-edit");
const form = listItem.querySelector(".form-edit");
textItem.textContent = inputItem.value;
listItem.classList.remove("editing");
form.remove();
TODOs = TODOs.map((todo) => {
if (todo.id === listItem.id) {
todo.title = inputItem.value;
}
return todo;
});
localStorage["data"] = JSON.stringify(TODOs);
}
Code language: JavaScript (javascript)
And it works!
Viewing Completed To-Dos
Before this, we could delete a to-do, but that was it. Even though our data structure was set up to have a complete
attribute that could change, all we did was filter
out the completed ones entirely from the data.
Here’s what that data structure is like:
{
title: `text of to-do`,
complete: false,
id: self.crypto.randomUUID()
}
Code language: JavaScript (javascript)
Now, when we check that checkbox in the UI to complete a to-do, we need to:
- Set
complete
totrue
if the list item is in the active list - Remove the list item entirely if the to-do is already in the completed list
We’ll update our function to be called toggleTodo
and do it like this:
function toggleTodo(event) {
const listItem = event.target.parentElement;
// Trigger complete animation
listItem.classList.toggle("complete");
setTimeout(() => {
// list item is already complete, remove it
if (listItem.dataset.complete === "true") {
TODOs = TODOs.filter((todo) => !todo.complete);
} else {
// list item is just being set to complete now
TODOs.forEach((todo) => {
if (todo.id === listItem.id) {
todo.complete = !todo.complete;
}
});
}
localStorage["data"] = JSON.stringify(TODOs);
if (!document.startViewTransition) {
buildUI();
} else {
document.startViewTransition(() => {
buildUI();
});
}
}, 1000);
}
Code language: JavaScript (javascript)
This isn’t a big change from last time, just one little fork in the logic that either removes it or updates the data.
Now we need a control though to decide if we’re looking at the active to-dos or the completed ones. Let’s make that control in HTML:
<div class="todo-type-toggles">
<button aria-pressed="true">Active</button>
<button>Completed</button>
</div>
Code language: HTML, XML (xml)
Now when you press the buttons, we’ll swap the state and re-build the UI accordingly:
const toggles = document.querySelectorAll(".todo-type-toggles > button");
toggles.forEach((toggle) => {
toggle.addEventListener("click", (event) => {
toggles.forEach((toggle) => {
toggle.setAttribute("aria-pressed", false);
});
toggle.setAttribute("aria-pressed", true);
if (toggle.textContent === states.ACTIVE) {
buildUI(states.ACTIVE);
} else {
buildUI(states.COMPLETED);
}
});
});
Code language: JavaScript (javascript)
Now I’m calling buildUI()
with a param to declare which type I want to see. I like using little ENUM type variables for this just to make sure we don’t do typos.
const states = {
ACTIVE: "Active",
COMPLETED: "Completed"
};
Code language: JavaScript (javascript)
Then we update the function to display one or the other…
function buildUI(state) {
let HTML = ``;
let viewTODOs = [];
if (state === states.COMPLETED) {
viewTODOs = TODOs.filter((todo) => todo.complete);
} else {
viewTODOs = TODOs.filter((todo) => !todo.complete);
}
if (viewTODOs.length === 0) {
HTML = `<li class="empty">Nothing to do!</li>`;
}
// Loop over the viewTODOs and build HTML to insert, exactly as before.
}
Code language: JavaScript (javascript)
This gives us an empty state as well.
And we’ve done it!
Perhaps more back-endy readers will be like “is this dude not sanitizing data before it goes to data storage?” and that would be a smart observation. We should probably be sanitizing the HTML. But for now, the only person you can pwn with this is yourself, so not a massive deal.
Here’s where we’ve gotten now:
Article Series
- Building a TODO App from Scratch — Step 1 — Planning & Design
- Building a TODO App from Scratch — Step 2 — HTML
- Building a TODO App from Scratch — Step 3 — Basic JavaScript Functionality
- Building a TODO App from Scratch — Step 4 — Styling & Interactive Choices
- Building a TODO App from Scratch — Step 5 — Extra Functionality