What you'll be able to do

By the end of this page you should be able to keep todos in an array, render them into the DOM, add new items from an input, and toggle a done flag — the classic beginner project done with clear structure.

Previous post: coding problems warmup.

Who this is for

  • People who know events and arrays separately but not together
  • Juniors who want one small portfolio-shaped exercise
  • Anyone tempted to jump to React before finishing DOM basics

You can skip the exact UI if you already built a todo — rebuild once with a single render() function for practice.

The shape: state → render → events

const todos = [];

function render() {
  // clear list, create elements from todos
}

form.addEventListener("submit", function (e) {
  e.preventDefault();
  // push into todos, then render()
});

Scenario A — add item. Mutate state, then render once.

Scenario B — toggle done. Update the object in the array, then render again.

The rule: the array is the truth; the DOM is a view you rebuild (fine at this size).

Minimal data model

// { id: number, text: string, done: boolean }

Scenario A — id from Date.now() is enough for a toy app.

Scenario B — duplicate text is allowed; ids keep toggles precise.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <form id="form">
      <input id="text" required placeholder="New todo" />
      <button type="submit">Add</button>
    </form>
    <ul id="list"></ul>
    <script>
      const todos = [];
      const list = document.getElementById("list");
      const form = document.getElementById("form");
      const input = document.getElementById("text");

      function render() {
        list.textContent = "";
        for (const todo of todos) {
          const li = document.createElement("li");
          const label = document.createElement("label");
          const box = document.createElement("input");
          box.type = "checkbox";
          box.checked = todo.done;
          box.addEventListener("change", function () {
            todo.done = box.checked;
            render();
          });
          label.appendChild(box);
          label.appendChild(document.createTextNode(" " + todo.text));
          if (todo.done) label.style.textDecoration = "line-through";
          li.appendChild(label);
          list.appendChild(li);
        }
      }

      form.addEventListener("submit", function (e) {
        e.preventDefault();
        const text = input.value.trim();
        if (!text) return;
        todos.push({ id: Date.now(), text: text, done: false });
        input.value = "";
        render();
      });
    </script>
  </body>
</html>

Stretch goals (optional)

Scenario A — delete button that filters by id.

Scenario B — filter All / Active / Done with a filter variable and re-render.

Mistakes I see a lot

1. Updating the DOM by hand in five places instead of one render.

2. Forgetting preventDefault on the form — page reloads, state vanishes.

3. Storing only strings then struggling to track done state.

4. Jumping to localStorage before render works — persist after the core loop is solid.

What to try before the next post

  1. Add delete.
  2. Add a count of remaining items.
  3. Sketch how you would save todos to localStorage (optional).

Next in this series: mini project — fetch list UI — loading remote data into a list with loading and error states.

Try this next outside the series

Practice goes further when you test the result and compare revisions instead of solving once and forgetting.