VS Vikesh Singh

Courses JavaScript Lesson 18

JAVASCRIPT · LESSON 18 OF 18

Building a Small Interactive App With Vanilla JS

· 4 min read

This final lesson pulls together everything from the course into one real, working project.

This final lesson pulls together everything from the course into one real, working project: a small interactive to-do list, built with plain vanilla JavaScript — no frameworks, no build tools, just HTML, CSS, and the concepts from lessons 1 through 17. Let's build it piece by piece and see exactly which earlier lesson each part leans on.

Setting Up The HTML And Selecting Elements

Start with the HTML structure and selecting our key elements, straight from the DOM lesson.

<input id="task-input" type="text" placeholder="Add a task">
<button id="add-task">Add</button>
<ul id="task-list"></ul>
const input = document.querySelector("#task-input");
const addButton = document.querySelector("#add-task");
const list = document.querySelector("#task-list");
let tasks = JSON.parse(localStorage.getItem("tasks")) || [];

That last line reuses the exact localStorage pattern from lesson 16 — read the saved tasks, or fall back to an empty array if nothing's there yet, so our to-do list survives a page reload instead of resetting every time.

Rendering The Task List

Next, a function to render the current task list to the page, using createElement and appendChild from the DOM lesson, plus map's cousin forEach from the array lesson.

function renderTasks() {
  list.innerHTML = "";
  tasks.forEach(function (task, index) {
    const item = document.createElement("li");
    item.textContent = task.text;
    if (task.done) {
      item.classList.add("done");
    }

    const removeBtn = document.createElement("button");
    removeBtn.textContent = "Remove";
    removeBtn.addEventListener("click", function () {
      removeTask(index);
    });

    item.appendChild(removeBtn);
    item.addEventListener("click", function (event) {
      if (event.target !== removeBtn) {
        toggleTask(index);
      }
    });

    list.appendChild(item);
  });
}

Clearing list.innerHTML = "" first and rebuilding everything is a simple, honest approach for a small list like this — not the most efficient possible technique for a huge list, but perfectly fine here, and far easier to reason about than trying to patch individual DOM elements. Notice the event delegation idea from the events lesson applied slightly differently: each <li> gets its own click listener here (fine at this small scale), and we check event.target against removeBtn specifically so clicking "Remove" doesn't also toggle the task as done — both handlers are attached to overlapping elements, so we need that check to tell them apart.

Adding, Toggling, And Removing Tasks

Now the functions that actually change our data, each one saving to localStorage immediately afterward so nothing is ever lost on reload.

function saveTasks() {
  localStorage.setItem("tasks", JSON.stringify(tasks));
}

function addTask(text) {
  if (text.trim() === "") {
    return;
  }
  tasks.push({ text: text, done: false });
  saveTasks();
  renderTasks();
}

function toggleTask(index) {
  tasks[index].done = !tasks[index].done;
  saveTasks();
  renderTasks();
}

function removeTask(index) {
  tasks = tasks.filter(function (task, i) {
    return i !== index;
  });
  saveTasks();
  renderTasks();
}

removeTask uses filter from the array lesson rather than splice — filtering out everything except the matching index is arguably more readable here than calculating splice's exact arguments. text.trim() === "" in addTask guards against adding a blank task made of only spaces, a small real-world validation detail easy to forget.

Wiring Up Event Listeners

Finally, wire up the events, using addEventListener and preventDefault-style thinking from the events lesson, plus a small async touch for realism.

addButton.addEventListener("click", function () {
  addTask(input.value);
  input.value = "";
  input.focus();
});

input.addEventListener("keydown", function (event) {
  if (event.key === "Enter") {
    addButton.click();
  }
});

async function init() {
  console.log("To-do app starting for Vikesh Singh");
  renderTasks();
}

init();

Listening for Enter on keydown, and simply triggering the add button's own click handler, avoids duplicating the add-task logic in two places — a small but genuinely useful habit: when two different triggers should do the exact same thing, make one of them call the other instead of copying the code.

What This Project Pulls Together

Step back and notice what this project actually demonstrates: DOM selection and creation (lesson 7), event listeners and delegation thinking (lesson 8), array methods like filter and forEach (lesson 6), closures implicitly, since every event listener here closes over index and tasks (lesson 10), localStorage persistence with JSON (lesson 16), and a guard clause using truthy/falsy logic (lesson 9). None of these lessons were really separate skills — they were always building toward being combined exactly like this.

One honest limitation worth naming directly: this whole app keeps its data in one browser's localStorage, so it won't show the same tasks on a different device or a different browser — that would require an actual backend server and a database, exactly the kind of thing the fetch lesson's POST example was pointing toward. Recognizing that boundary — what a purely client-side, vanilla JavaScript app can and can't do on its own — is itself a useful, realistic takeaway to end this course on, since most real products eventually need both sides working together.

A final practical habit for any project, not just this one: before adding a new feature, decide where its logic belongs among your existing functions, rather than bolting it onto whichever function happens to be nearby. Keeping addTask, toggleTask, and removeTask each doing exactly one clear thing, as this project does, is what makes a small app stay easy to extend rather than turning into a tangle the moment a new feature gets added.

A small challenge: extend this to-do app with one more feature using only what you've learned in this course — a "clear completed" button using filter, or a task counter showing how many remain using a simple reduce or length check. Building past a tutorial's exact scope, even by one small feature, is the real test of whether these concepts have actually stuck.