JAVASCRIPT / LESSON 15

Build a small to-do list

Combine form events, text input, and DOM creation in a usable project.

Content updated

By the end, you can…

  • Read the input, validate it, update the page, and test the entire add-and-remove flow.
  • Predict the example's result, change one part, and verify the behavior in your own script.
Before you begin

One idea at a time

This project combines ideas you have practiced separately. A labeled input collects a task, a form handles submission, and a list shows the result. Each task gets an explicit Remove button. Using real form controls makes keyboard interaction work without requiring a user to guess that clicking text deletes it.

Start with a deliberately small version: tasks exist only in the current page and disappear on reload. This keeps the focus on event flow and DOM updates. After the basic behavior works, you can separately design a data array and persistence. Saving half-finished DOM markup is not a good substitute for a clear data model.

What to remember first

Remember first

Read the input, validate it, update the page, and test the entire add-and-remove flow.

Read the code before trying it

form.addEventListener("submit", callback)Handle button or Enter
Listen on the form, then prevent its default navigation for this local interaction.
input.value.trim()Read current text
Read inside the callback so each submission sees the latest input. trim removes surrounding spaces.
item.append(text, remove)Assemble one list item
append can insert several nodes. These nodes were created separately with createElement.
item.remove()Remove the selected task
Each callback keeps a reference to its own item, so it removes the intended row. list.children is the current collection of direct child elements; list.children.length counts them after an add or removal.
input.focus()Return to a useful control
After adding or removing, put keyboard focus back on the input instead of leaving it on a detached button.
role="status"Announce feedback
The status paragraph reports additions and removals without requiring focus to move to it.

Walk through an example

html
<form id="todo-form">
  <label for="task">New task</label>
  <input id="task" name="task">
  <button type="submit">Add task</button>
</form>
<ul id="tasks"></ul>
<p id="status" role="status">No tasks yet.</p>
javascript
const form = document.querySelector("#todo-form");
const input = document.querySelector("#task");
const list = document.querySelector("#tasks");
const status = document.querySelector("#status");

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const title = input.value.trim();
  if (title === "") {
    status.textContent = "Please enter a task.";
    return;
  }

  const item = document.createElement("li");
  const text = document.createElement("span");
  text.textContent = title + " ";
  const remove = document.createElement("button");
  remove.type = "button";
  remove.textContent = "Remove " + title;
  remove.addEventListener("click", () => {
    item.remove();
    status.textContent = "Removed: " + title;
    input.focus();
  });
  item.append(text, remove);
  list.append(item);
  status.textContent = "Added: " + title;
  input.value = "";
  input.focus();
});

On submission, the handler reads and trims the current input. Empty text produces a reminder and returns early. Otherwise it creates a li, a span, and a button. The removal callback remembers that particular li and title even after the submit callback finishes. This retained access is called a closure. The item is appended, the input is cleared, and focus is returned for the next task.

See it happen: try a small change

Build a small to-do list: try it

Predict the result, choose a version, then compare the code and output. Try the controls when the example has them. Each version starts fresh.

Code to notice

const form = document.querySelector("#todo-form");
const input = document.querySelector("#task");
const list = document.querySelector("#tasks");
const status = document.querySelector("#status");

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const title = input.value.trim();
  if (title === "") {
    status.textContent = "Please enter a task.";
    return;
  }

  const item = document.createElement("li");
  const text = document.createElement("span");
  text.textContent = title + " ";
  const remove = document.createElement("button");
  remove.type = "button";
  remove.textContent = "Remove " + title;
  remove.addEventListener("click", () => {
    item.remove();
    status.textContent = "Removed: " + title;
    input.focus();
  });
  item.append(text, remove);
  list.append(item);
  status.textContent = "Added: " + title;
  input.value = "";
  input.focus();
});

Live result

Lesson example
index.htmlPreview

What changed: Try blank input, Enter, duplicate names, and each Remove button. This working list resets on reload and stores nothing.

Look a little closer

textContent treats a task such as <b>read</b> as literal text. It does not turn the user input into markup. This matters whenever text comes from a user or external data. The button's type is explicitly button so it will not accidentally submit a form if the markup is rearranged later.

A closure lets the removal callback remember variables from the submit call that created it. Each submission creates new item and title bindings, so each Remove button keeps the right pair. Test two tasks with the same text: their separate DOM nodes should still be removable independently. A future stored-data version should give records stable identifiers.

Try it in your own project

Copy the form, list, and status paragraph into body and the supplied script into script.js. Test add and remove before adding the count. Create a separate paragraph for the count, select it, and assign list.children.length after both list.append(item) and item.remove(). Reload starts with zero tasks.

The interactive preview runs in its own isolated document. Its Output panel mirrors console.log messages; it is not a full DevTools console. Choosing another version or resetting starts a fresh example and does not edit your local files. Read any simulation label: storage and request demonstrations explain their offline substitutes, while the written local examples use the real browser APIs.

A common wrong turn

Attaching another submit listener during each submission would multiply the handlers. Attach it once during setup. Also avoid rebuilding the whole list with user-supplied innerHTML; create nodes and set their text so input stays safe and each button's behavior remains explicit.

Your turn: build a small to-do list

Rebuild the to-do list locally and add a visible count of remaining tasks.

Try this in your own HTML and JavaScript files, then open the page in a browser. There is no separate checked practice for this task yet.

  1. Copy the HTML into body and JavaScript into your deferred script. Add two tasks with Enter.
  2. Test blank input, surrounding spaces, duplicate task names, and text containing angle brackets. Remove only one duplicate.
  3. Add a count using list.children.length after both add and remove. Check the count returns to zero when all tasks are removed.

Go to the source

Examples are authored for Tuklas and checked with code and reference documentation. This is not a claim of independent human review. Suggest a correction.