Build Task Triage with JavaScript

Build a task-triage app with JavaScript state, validation, and filters.

Introduction

30 Second Summary

Small task lists stop feeling useful when blank rows sneak in or the remaining count drifts out of sync. A tool earns your trust when every click produces the result you expect.

In this project, you will build Task Triage with plain JavaScript. The finished app turns typed text into tasks you can complete, undo, or delete while its count stays accurate.

What You'll Build

You will demo a task list that reacts immediately to every addition, completion, undo, or deletion.

By the end of this project, you'll have:

  • A labeled task form with input validation that adds real tasks while rejecting whitespace-only submissions with a clear message.
  • Task controls for completing, undoing, or deleting items with a synchronized remaining count that always matches the task data.
  • A browser breakpoint that pauses the completion handler so you can inspect application state before the page updates.
  • Secret Mission: Add All, Active, and Completed filters that change the visible task list without changing the underlying task data.

Are there any prerequisites?

Basic familiarity with HTML and CSS syntax is enough because no JavaScript framework knowledge is required.

Your Windows computer already has Visual Studio Code plus either Google Chrome or Microsoft Edge, so no extra installation is required.

Before We Start

This checkpoint helps you commit to building a Task Triage app with vanilla JavaScript. You will practice connecting browser events to application state changes and visible updates in the DOM.

Set Up the Project

Task Triage needs a dependable home before its JavaScript can connect browser events to visible updates.

A three-file workspace in Visual Studio Code keeps every change visible.

Google Chrome or Microsoft Edge can open the HTML directly. The document also links the CSS file plus the JavaScript file.

In this step, get ready to:
  • Create the task-triage project folder.
  • Add the three local project files.
  • Load the linked starter page directly in your browser.
Create the project workspace

A folder opened in Visual Studio Code becomes a workspace. Its Explorer view keeps every project file together.

Why Plain Files?

Frameworks add component syntax plus build tooling. This project uses plain files so you can see how each browser event reaches your own JavaScript.

The small workspace keeps the focus on events, state, and DOM rendering. Every visible change maps back to code you wrote.

  • Press the Windows key to open Search.
  • Type Visual Studio Code into Search.

Windows Search lists Visual Studio Code as an app result.

  • Press Enter to open Visual Studio Code.

You will see the Visual Studio Code window.

  • Select File from the top menu.
  • Select Open Folder....

A folder picker opens so you can choose where the workspace lives.

  • Select Desktop in the folder picker.
  • Select New Folder.

An editable folder name appears on your Desktop.

  • Type task-triage as the folder name.
  • Press Enter to create the folder.

The new task-triage folder is now selected.

  • Select Select Folder to open the workspace.
  • Confirm that you trust the folder if Workspace Trust appears.

The Explorer view now shows task-triage as the open workspace.

  • Select the New File... button at the top of the Explorer view.

A filename field opens under task-triage.

  • Type index.html into the filename field.
  • Press Enter to create the file.

You will see index.html in the Explorer view.

  • Select the New File... button again.

Another filename field opens inside the same workspace.

  • Type styles.css into the filename field.
  • Press Enter to create the file.

You will see styles.css beside index.html.

  • Select the New File... button once more.

The last filename field opens under the other files.

  • Type script.js into the filename field.
  • Press Enter to create the file.

Your Explorer view now lists all three project files.

Add the starter document

The HTML document gives the browser a page to display. Its link and script elements connect the empty files you will build in later steps.

  • Select index.html in the Explorer view.
  • Add the linked starter document by pasting this code:
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Task Triage</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <h1>Task Triage: JavaScript Foundation Lab</h1>

    <script src="script.js"></script>
  </body>
</html>

What Does This Code Do?

  • The document declaration plus lang="en-US" identify the page as an English HTML document.
  • The link element connects styles.css to the page.
  • The h1 element gives the starter page a visible heading.
  • The script element loads script.js at the end of the page body.
  • Press Ctrl+S to save index.html.
  • Check the Explorer view for index.html, styles.css, and script.js.

Missing a File or Link?

Check that all three files sit directly inside task-triage. Make sure each filename matches the spelling in index.html.

If the HTML indentation or colours look unusual, compare your opening plus closing tags with the reference below.

Help me check my Task Triage file structure and HTML links.

The reference below shows the complete file state for this checkpoint.

✔️ Awesome, I've got everything!

Your index.html file is saved. The styles.css plus script.js files remain empty at this checkpoint.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Task Triage</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <h1>Task Triage: JavaScript Foundation Lab</h1>

    <script src="script.js"></script>
  </body>
</html>

This reference is the complete index.html file for this step. Both styles.css plus script.js are empty.

Open the starter page

A browser can load this project through a local file: URL. The page needs no runtime, package manager, or development server.

  • Right-click index.html in the Visual Studio Code Explorer view.
  • Select Reveal in File Explorer.

File Explorer opens with index.html highlighted inside task-triage.

  • Press the Windows key to open Search.
  • Type Google Chrome or Microsoft Edge into Search.

Windows Search lists your chosen browser as an app result.

  • Press Enter to open the browser.

Your chosen browser opens in a separate window.

Before you load the file: which heading do you expect the browser to show.

  • Drag index.html from File Explorer into the browser window.

You will see the heading Task Triage: JavaScript Foundation Lab. That first visible result proves the browser loaded your linked local project.

Do You See a Blank Page?

Confirm that you dragged index.html from the task-triage folder. Check that you saved the file after pasting the starter document.

Help me find why my local Task Triage page is blank.

Your three-file project shell now works directly in the browser. Next, you will turn that blank shell into the visible Task Triage interface.

Build the Visible Interface

Your starter page proves that your linked HTML, CSS, and JavaScript files load together. Now you can turn that starter page into the interface people use.

JavaScript needs meaningful page elements before it can respond to form submissions or update the task list. This step creates those DOM targets.

In this step, get ready to:
  • Replace the starter heading with the semantic Task Triage interface.
  • Style the form plus the task area with the final base CSS.
  • Verify that the native form controls are reachable with the keyboard.
Build the semantic page structure

Semantic HTML gives the task form a clear structure. The matching label plus input identifiers create an explicit association for the New task field.

  • In the Visual Studio Code Explorer sidebar, select index.html.
  • Find the opening <body> tag.
  • Replace everything from <body> through </body> with the structure below:
  <body>
    <main class="app">
      <h1>Task Triage</h1>
      <p>Practice turning decisions into JavaScript.</p>

      <form id="task-form">
        <label for="task-input">New task</label>
        <div class="form-row">
          <input id="task-input" name="task" type="text" />
          <button type="submit">Add task</button>
        </div>
        <p id="error-message" class="error"></p>
      </form>

      <div class="list-header">
        <h2>Your tasks</h2>
        <p id="task-count">0 tasks remaining</p>
      </div>

      <ul id="task-list"></ul>
    </main>

    <script src="script.js"></script>
  </body>

What Does This HTML Do?

  • The main.app element groups the complete interface into one page region.
  • The matching for="task-input" and id="task-input" values connect the label to the text field.
  • The task-form identifier gives the next step a precise form to select.
  • The empty error-message and task-list elements reserve destinations for future updates.
  • The script stays at the end of the body so the page elements exist before the browser loads script.js.
  • Save index.html.
  • Switch back to Google Chrome or Microsoft Edge.
  • Refresh the page.

Good start. You should see the Task Triage heading above a labeled field plus an Add task button.

Still Seeing the Starter Heading?

  • Confirm that you saved index.html inside the task-triage folder.
  • Check that the browser still has the same index.html file open.
  • Compare the opening plus closing body tags with the reference below.

Help me find why my updated HTML is not appearing

✔️ Awesome, I've got everything!

Your semantic page structure is saved in index.html.

ⓧ I'd like to double check the full code

  • Compare your complete index.html file with this reference:
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Task Triage</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="app">
      <h1>Task Triage</h1>
      <p>Practice turning decisions into JavaScript.</p>

      <form id="task-form">
        <label for="task-input">New task</label>
        <div class="form-row">
          <input id="task-input" name="task" type="text" />
          <button type="submit">Add task</button>
        </div>
        <p id="error-message" class="error"></p>
      </form>

      <div class="list-header">
        <h2>Your tasks</h2>
        <p id="task-count">0 tasks remaining</p>
      </div>

      <ul id="task-list"></ul>
    </main>

    <script src="script.js"></script>
  </body>
</html>
Style the interface

The box model controls the space around each element. Flexbox keeps related controls aligned as the page width changes.

  • In the Visual Studio Code Explorer sidebar, select styles.css.
  • Add the page foundation by pasting this code into the empty file:
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: Arial, sans-serif;
  background-color: #f3f4f6;
  color: #1f2937;
}

button,
input {
  font: inherit;
}

.app {
  max-width: 640px;
  margin: 0 auto;
  padding: 24px;
  background-color: #ffffff;
  border: 1px solid #d1d5db;
  border-radius: 12px;
}

What Does This CSS Do?

  • The universal selector makes declared widths include padding plus borders.
  • The body rules create space around the app while setting the page colors.
  • The input plus button inherit the page font for consistent text.
  • The .app rules turn the main region into a centered card with a maximum width.
  • Save styles.css.
  • Refresh the browser.

The visual foundation is in place. You should see a white card centered against a light gray page.

Still Seeing an Unstyled Page?

  • Confirm that styles.css is saved inside the task-triage folder.
  • Check that index.html still contains href="styles.css".
  • Remove any accidental characters before the first * selector.

Help me diagnose why my styles.css file is not styling Task Triage

  • Add the responsive row layout below the .app block by pasting:
.form-row,
.list-header,
.task-item,
.task-actions {
  display: flex;
  align-items: center;
}

.form-row,
.task-actions {
  gap: 8px;
}

.list-header,
.task-item {
  justify-content: space-between;
  gap: 16px;
}

input {
  flex: 1;
  min-width: 0;
  padding: 10px;
  border: 1px solid #9ca3af;
  border-radius: 6px;
}

How Does the Layout Work?

  • The shared selectors turn the form row plus list header into flex containers.
  • The gap values keep neighboring controls separated.
  • The justify-content rule places the list heading opposite the task counter.
  • The input uses the available row width while preserving its border plus internal spacing.
  • Save styles.css.
  • Refresh the browser.

That layout is taking shape. The input should stretch across the form row while the counter sits opposite Your tasks.

Is the Input Still Narrow?

  • Check that input contains flex: 1;.
  • Confirm that .form-row appears in the selector using display: flex;.
  • Look for missing commas between the shared selectors.

Help me fix the form row layout in my Task Triage CSS

  • Add the button plus empty-list styles below the input block by pasting:
button {
  padding: 10px 14px;
  border: 0;
  border-radius: 6px;
  background-color: #2563eb;
  color: #ffffff;
  cursor: pointer;
}

button:hover {
  background-color: #1d4ed8;
}

#task-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

How Are the Controls Styled?

  • The button rules create a blue control with a larger clickable area.
  • The hover rule darkens the button when the pointer moves over it.
  • The task-list rules remove the browser's default list spacing plus markers.
  • Save styles.css.
  • Refresh the browser.
  • Move your pointer over the Add task button.

Your main control now has its finished appearance. You should see the blue button darken while the pointer is over it.

Does the Button Stay Unstyled?

  • Confirm that the selector starts with button.
  • Check each color value for a leading # character.
  • Make sure the hover selector contains the colon in button:hover.

Help me find why my Task Triage button styles are not appearing

  • Finish the stylesheet by adding these task-state styles below the #task-list block:
.task-item {
  padding: 12px 0;
  border-top: 1px solid #e5e7eb;
}

.task-actions button:last-child {
  background-color: #b91c1c;
}

.completed .task-text {
  color: #6b7280;
  text-decoration: line-through;
}

.error {
  min-height: 24px;
  color: #b91c1c;
}

Why Add Task-State Styles Now?

  • The .task-item rules separate future task rows with spacing plus a border.
  • The last action button receives the red delete color.
  • The completed class gives finished task text a muted strikethrough treatment.
  • The error area keeps a stable height while reserving red text for validation feedback.
  • Save styles.css.
  • Refresh the browser.

The final styling pass is done. You should see a small reserved space below the form before the task-list heading.

Is the Form Spacing Missing?

  • Confirm that the error paragraph still has class="error" in index.html.
  • Check that .error includes min-height: 24px;.
  • Look for a missing closing brace in the preceding CSS block.

Help me debug the error-area spacing in Task Triage

✔️ Awesome, I've got everything!

Your complete stylesheet is saved. The Task Triage interface now has its final base appearance.

ⓧ I'd like to double check the full code

  • Compare your complete styles.css file with this reference:
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: Arial, sans-serif;
  background-color: #f3f4f6;
  color: #1f2937;
}

button,
input {
  font: inherit;
}

.app {
  max-width: 640px;
  margin: 0 auto;
  padding: 24px;
  background-color: #ffffff;
  border: 1px solid #d1d5db;
  border-radius: 12px;
}

.form-row,
.list-header,
.task-item,
.task-actions {
  display: flex;
  align-items: center;
}

.form-row,
.task-actions {
  gap: 8px;
}

.list-header,
.task-item {
  justify-content: space-between;
  gap: 16px;
}

input {
  flex: 1;
  min-width: 0;
  padding: 10px;
  border: 1px solid #9ca3af;
  border-radius: 6px;
}

button {
  padding: 10px 14px;
  border: 0;
  border-radius: 6px;
  background-color: #2563eb;
  color: #ffffff;
  cursor: pointer;
}

button:hover {
  background-color: #1d4ed8;
}

#task-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.task-item {
  padding: 12px 0;
  border-top: 1px solid #e5e7eb;
}

.task-actions button:last-child {
  background-color: #b91c1c;
}

.completed .task-text {
  color: #6b7280;
  text-decoration: line-through;
}

.error {
  min-height: 24px;
  color: #b91c1c;
}
Verify keyboard access

Native form controls participate in the browser's keyboard order automatically. This lets someone reach the input plus submit button without using a pointer.

Before you test the tab order, which control do you expect to receive focus first?

  • Click the empty gray area beside the white app card.
  • Press Tab once.

You should see a focus indicator around the labeled New task input.

  • Press Tab once more.

You should see the focus indicator move to the Add task button. The page should also show Your tasks plus 0 tasks remaining.

Does Tab Skip a Control?

  • Confirm that the input still uses the native <input> element.
  • Check that the button still uses type="submit".
  • Click the page area before restarting the tab-order test.

Help me debug the keyboard tab order in my Task Triage form

You now have a styled interface with keyboard-reachable controls. Next, you will connect the form to JavaScript so submitted text becomes a visible task row.

Expose the Empty-Task Bug

The page has the controls your task app needs. Now you'll connect a form submission to JavaScript so one user event can change the DOM.

The first version keeps every change inside one temporary event listener. You'll test that design in the browser before deciding what stronger logic needs to handle.

In this step, get ready to:
  • Capture references to the form controls and task list.
  • Create a temporary submit handler that appends each entry to the list.
  • Compare a normal task with a whitespace-only submission.
Connect the form to the page

A DOM reference gives JavaScript access to an element that already exists in index.html. The submit handler can use those references to read the input and add a new list row.

  • In Visual Studio Code, select script.js from the Explorer sidebar.
  • Add the temporary form handler by pasting this code into script.js:
const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");

taskForm.addEventListener("submit", (event) => {
  event.preventDefault();

  const listItem = document.createElement("li");
  listItem.classList.add("task-item");
  listItem.textContent = taskInput.value;
  taskList.appendChild(listItem);

  taskInput.value = "";
  taskInput.focus();
});

What does this code do?

  • taskForm stores the form reference.
  • taskInput stores the text input reference.
  • taskList stores the task list reference.
  • preventDefault() keeps the form submission on the current page.
  • Each submission creates a list item from the raw input value.
  • The final assignment clears the input.
  • The focus call returns keyboard focus to the input.
  • Save script.js.
  • Switch back to index.html in your browser from earlier.
  • Refresh the page.

You'll see the styled Task Triage interface reload with an empty task list.

Submit a normal task

A normal task gives the handler readable text to place inside its new list item. This test proves the form event reaches the page.

  • Enter Read one MDN page in the New task field.
  • Click Add task.

Your first event-to-DOM path is live: Read one MDN page now sits in a visible task row.

You'll also see an empty input with keyboard focus restored.

Don't see the task row?

  • Confirm that script.js is saved.
  • Check that the three selectors match the element IDs in index.html.
  • Confirm that script.js remains linked at the end of the page body.

Help me debug why the temporary submit handler does not add a visible task row.

✔️ Awesome, I've got everything!

Great. Double-check that script.js is saved before you test the edge case.

ⓧ I'd like to double check the full code

const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");

taskForm.addEventListener("submit", (event) => {
  event.preventDefault();

  const listItem = document.createElement("li");
  listItem.classList.add("task-item");
  listItem.textContent = taskInput.value;
  taskList.appendChild(listItem);

  taskInput.value = "";
  taskInput.focus();
});
Test a whitespace-only task

The next input contains characters without readable text. That makes it a useful test of what the handler accepts.

Before you submit the spaces, do you think the handler will add another task row?

  • Select the New task input.
  • Press Space five times.
  • Click Add task.

You'll see Read one MDN page followed by a second row with a border but no readable text. The blank row proves that the handler accepts raw whitespace as task content.

Why did the blank row appear?

The handler copies the raw input value directly into a new list item. The row remains visible because the task-item class adds padding and a border.

The handler never uses trim() to remove surrounding whitespace. It never rejects the empty result.

The page also has no separate task state array. The DOM is the only record of what the learner submitted.

  • Record the cause of the blank row in the checkpoint below.

You've reproduced the logic flaw with a clear test case. Next, you'll replace this fragile path with validated task state and a repeatable render function.

Model State and Fix the Logic

The blank row you reproduced exposed a gap in your JavaScript logic. The temporary handler accepts any string because the page is its only record.

This step introduces application state as the source of truth. A render function turns that state into the DOM after every accepted event.

In this step, get ready to:
  • Store valid tasks as objects in an array.
  • Render task rows from state while keeping the remaining count synchronized.
  • Control each task through its action buttons.
Store tasks as state

The tasks array becomes the app's source of truth. renderTasks() rebuilds the visible list from that array.

  • In script.js, replace the three document.querySelector() declarations with this state and rendering foundation:
const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const errorMessage = document.querySelector("#error-message");

let tasks = [];

function renderTasks() {
  taskList.textContent = "";

  for (const task of tasks) {
    const listItem = document.createElement("li");
    const taskText = document.createElement("span");

    listItem.classList.add("task-item");
    taskText.classList.add("task-text");

    taskText.textContent = task.text;
    listItem.appendChild(taskText);
    taskList.appendChild(listItem);
  }

  const remainingTasks = tasks.filter((task) => !task.completed).length;
  taskCount.textContent = `${remainingTasks} task${remainingTasks === 1 ? "" : "s"} remaining`;
}

What Does This Foundation Do?

  • The two new selectors connect JavaScript to the existing error message and remaining-task counter.
  • The tasks array holds the data that controls the interface.
  • The loop creates one visible row for every object in tasks.
  • The final two lines count incomplete tasks and update the text beside Your tasks.
  • Replace the existing taskForm.addEventListener() block with this validated submit handler:
taskForm.addEventListener("submit", (event) => {
  event.preventDefault();

  const taskText = taskInput.value.trim();

  if (taskText === "") {
    errorMessage.textContent = "Enter a task before submitting.";
    taskInput.focus();
    return;
  }

  tasks.push({
    text: taskText,
    completed: false,
  });

  taskInput.value = "";
  errorMessage.textContent = "";
  renderTasks();
  taskInput.focus();
});

renderTasks();

How Does Validation Protect State?

  • The trim() call removes whitespace from both ends of the submitted text.
  • An empty result displays the validation message before the handler reaches tasks.push().
  • A valid result becomes an object with text and completed properties.
  • The call to renderTasks() turns the updated array into a fresh task list.
  • Save script.js.
  • Switch back to the browser tab from the previous step.

Before you refresh, make a mental prediction about whether a spaces-only submission can still create a row.

  • Refresh the page.
  • Enter Read one MDN page in the New task field.
  • Click Add task.
  • Press the Spacebar three times in the New task field.
  • Click Add task again.

You should see one task with 1 task remaining. The spaces-only submission should show Enter a task before submitting. without creating another row.

That bug is now contained. Whitespace never enters your task state.

Still Seeing a Blank Row?

Check that taskInput.value.trim() includes the parentheses after trim.

Make sure the empty-string check appears before tasks.push().

Help me fix my task validation

Add completion and deletion controls

Each task object already records whether it is complete. The row buttons now update that state before calling renderTasks() again.

  • Inside renderTasks(), find the for (const task of tasks) loop.
  • Replace the code from const listItem = document.createElement("li"); through taskText.textContent = task.text; with this expanded row setup:
    const listItem = document.createElement("li");
    const taskText = document.createElement("span");
    const taskActions = document.createElement("div");
    const completeButton = document.createElement("button");
    const deleteButton = document.createElement("button");

    listItem.classList.add("task-item");
    taskText.classList.add("task-text");
    taskActions.classList.add("task-actions");

    if (task.completed) {
      listItem.classList.add("completed");
    }

    taskText.textContent = task.text;
    completeButton.textContent = task.completed ? "Undo" : "Complete";
    deleteButton.textContent = "Delete";

How Does Each Row Reflect State?

  • Each task receives a text element plus a container for its two action buttons.
  • A completed task receives the completed class that activates the existing crossed-out style.
  • The completion button reads Complete or Undo according to the current value of task.completed.
  • Find listItem.appendChild(taskText); inside the same loop.
  • Replace the two existing append lines with this button behavior and row assembly:
    completeButton.addEventListener("click", () => {
      task.completed = !task.completed;
      renderTasks();
    });

    deleteButton.addEventListener("click", () => {
      tasks = tasks.filter((currentTask) => currentTask !== task);
      renderTasks();
    });

    taskActions.appendChild(completeButton);
    taskActions.appendChild(deleteButton);
    listItem.appendChild(taskText);
    listItem.appendChild(taskActions);
    taskList.appendChild(listItem);

What Do the Buttons Change?

  • The completion listener reverses the current completed value for its task.
  • The deletion listener replaces tasks with an array that excludes the selected object.
  • Both listeners call renderTasks() so the row styling and remaining count reflect the new state.
  • The final lines assemble the text and buttons into a visible task row.
  • Save script.js.
  • Switch back to the browser tab.

Before you refresh, predict how the button label and counter should react when you complete a task.

  • Refresh the page.
  • Enter Pay electricity bill in the New task field.
  • Click Add task.

You should see the task with Complete and Delete buttons. The counter should read 1 task remaining.

  • Click Complete beside the task.
  • Click Undo beside the task.
  • Click Delete beside the task.

Completion should cross out the task and change the counter to 0 tasks remaining. Undo should restore the row before Delete removes it.

Buttons Not Updating the Task?

Check that both click listeners call renderTasks() after changing tasks.

Make sure the completion listener updates task.completed for the task from the current loop.

Help me debug my task buttons

✔️ Awesome, I've got everything!

Great. Save script.js before running the final workflow check.

ⓧ I'd like to double check the full code

const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const errorMessage = document.querySelector("#error-message");

let tasks = [];

function renderTasks() {
  taskList.textContent = "";

  for (const task of tasks) {
    const listItem = document.createElement("li");
    const taskText = document.createElement("span");
    const taskActions = document.createElement("div");
    const completeButton = document.createElement("button");
    const deleteButton = document.createElement("button");

    listItem.classList.add("task-item");
    taskText.classList.add("task-text");
    taskActions.classList.add("task-actions");

    if (task.completed) {
      listItem.classList.add("completed");
    }

    taskText.textContent = task.text;
    completeButton.textContent = task.completed ? "Undo" : "Complete";
    deleteButton.textContent = "Delete";

    completeButton.addEventListener("click", () => {
      task.completed = !task.completed;
      renderTasks();
    });

    deleteButton.addEventListener("click", () => {
      tasks = tasks.filter((currentTask) => currentTask !== task);
      renderTasks();
    });

    taskActions.appendChild(completeButton);
    taskActions.appendChild(deleteButton);
    listItem.appendChild(taskText);
    listItem.appendChild(taskActions);
    taskList.appendChild(listItem);
  }

  const remainingTasks = tasks.filter((task) => !task.completed).length;
  taskCount.textContent = `${remainingTasks} task${remainingTasks === 1 ? "" : "s"} remaining`;
}

taskForm.addEventListener("submit", (event) => {
  event.preventDefault();

  const taskText = taskInput.value.trim();

  if (taskText === "") {
    errorMessage.textContent = "Enter a task before submitting.";
    taskInput.focus();
    return;
  }

  tasks.push({
    text: taskText,
    completed: false,
  });

  taskInput.value = "";
  errorMessage.textContent = "";
  renderTasks();
  taskInput.focus();
});

renderTasks();
Verify every state transition

The final check follows each path that can change the task array. The visible rows and remaining count should agree after every action.

Before you begin, predict which actions should change the remaining-task count.

  • Refresh the page to reset the in-memory task list.
  • Enter Plan weekly meals in the New task field.
  • Click Add task.
  • Enter Read one MDN page in the New task field.
  • Click Add task.
  • Press the Spacebar three times in the New task field.
  • Click Add task.

You should see two task rows with 2 tasks remaining. The validation message should appear without adding a third row.

  • Click Complete beside Plan weekly meals.

You should see the first task crossed out with an Undo button. The counter should read 1 task remaining.

  • Click Undo beside Plan weekly meals.

You should see the crossed-out style disappear. The counter should return to 2 tasks remaining.

  • Click Delete beside Read one MDN page.

You should see one active task left with 1 task remaining. The list and counter now stay synchronized after every supported action.

Strong finish. Your app now validates input and renders every visible change from task state.

Your state-driven app is working. Next, you will pause its completion handler in DevTools to inspect the state change while it happens.

Trace the Logic with DevTools

Your Task Triage app now keeps its visible list synchronized with array state. The finished screen hides the order behind each state change.

Chrome DevTools or Microsoft Edge DevTools lets you pause JavaScript before one line runs. You will inspect the data on both sides of the completion assignment.

In this step, get ready to:
  • Set a line-of-code breakpoint on the completion assignment in script.js.
  • Inspect the task object before the completion assignment runs.
  • Resume execution to confirm renderTasks() updates the task row and remaining count.
Pause the completion handler

A line-of-code breakpoint pauses execution before the selected statement runs. That pause gives you a stable moment to inspect the current values.

  • Switch back to the browser tab running Task Triage.
  • Enter Trace completion in the New task field.
  • Click Add task.

You will see Trace completion as an active task row. Its Complete button gives the debugger a click to trace.

Finding script.js in a local page can be fiddly because the source tree may contain several levels.

  • Press F12 to open browser DevTools.
  • Select the Sources panel or tool.
  • Select script.js in the page source tree.
  • Scroll to the click listener attached to completeButton inside renderTasks().
  • Click the line-number column beside the assignment that flips task.completed.

That is your first debugger checkpoint set. You will see a marker on the selected line number.

What does a breakpoint show?

A breakpoint pauses the completion handler before the selected assignment executes. The paused state lets you compare the same task immediately before and after its completed property changes.

Inspect the state transition

Before you click Complete, what value do you expect for task.completed when DevTools first pauses.

  • Return to the Task Triage page area without closing DevTools.
  • Click Complete for the Trace completion task.

You will see DevTools pause before the assignment runs. The task.completed value has not changed at this point.

The task row still looks active. The remaining count still includes this task.

Breakpoint not pausing?

  • Confirm the breakpoint marker remains beside the completion assignment in script.js.
  • Confirm you clicked Complete for the Trace completion task.

Help me find out why my completion breakpoint is not pausing.

  • In the Scope pane, expand the entries until task is visible.
  • Expand the task object.
  • Expand the tasks array.

You will see task.completed set to false. The tasks array contains that same task object.

  • Use Step over next function call once.

You will see task.completed change to true while execution remains paused. The matching object in tasks shows the same new value.

Resume and verify the render

Before you resume execution, which visible parts of the app do you expect renderTasks() to update.

  • Use the debugger's resume control to continue execution.

You will see the Trace completion task crossed out. Its action changes from Complete to Undo.

The remaining-task count decreases by one. This confirms renderTasks() used the updated array state.

  • In the Sources panel or tool, click the breakpoint marker again to remove it.

You will see the marker disappear from the line-number column. The completion handler can now run without pausing.

You have now watched the completion state change before the page redraws. That is the debugger turning hidden execution into evidence you can explain.

Secret mission

Filter Tasks by Status

Your task data can power several focused views. Add All, Active, and Completed filters while keeping every task object in the same array.

Clean Up Your Resources

Clean Up Your Resources

Everything in this project lives in the local task-triage folder. There are no cloud resources or ongoing costs. Use the options below to decide what happens to the folder.

Resources you used:

  • The local task-triage folder containing index.html, styles.css, and script.js.

Keep everything running

Keeping the project requires no cleanup. Choose this option while you are still experimenting with task state or filters.

  • Keep index.html, styles.css, and script.js together inside the task-triage folder.
  • Continue testing the Task Triage app in the browser from earlier.

Pause - I'll come back to this later

Pausing closes the current editing session while preserving every project file for later.

  • Save each open project file in Visual Studio Code.
  • Close the Task Triage browser tab.
  • Close Visual Studio Code.

The task-triage files stay on your computer. Tasks entered into the app reset when you reopen or refresh the page.

Delete - I don't want to use this again

Permanent deletion removes the entire project folder. This option fits when you are comfortable losing all three files.

  • Close the Task Triage browser tab.
  • Close Visual Studio Code.
  • Use Windows search to open File Explorer.
  • Navigate to the location where you created the task-triage folder.
  • Select the task-triage folder.
  • Permanently delete the selected folder using File Explorer's delete controls.
  • Confirm the permanent deletion if Windows asks.
  • Check the same location. You should no longer see the task-triage folder.

Nice Work!

Nice Work!

Task Triage is complete! Your browser app now turns every form or button event into a predictable JavaScript state change.

You've learned how to:

  • Built an interactive task app that turns labeled form submissions into visible task rows.
  • Used validated array state with state-driven DOM rendering to synchronize task controls with the remaining count.
  • Traced a completion click with a DevTools breakpoint to inspect task before the browser updated the interface.
  • Completed the Secret Mission by deriving All, Active, and Completed filters from the unchanged tasks array.

Ready to quiz yourself?