DOM Manipulation in JavaScript: Build an Interactive Study Planner

Build a study planner while learning to select nodes, update safe content, delegate clicks, add tasks, and verify every visible total.

KnowledgeGate Team

Exam prep & CS education

Updated 6 Sep 20265 min read

Your JavaScript variables can change while the page still looks unchanged. DOM manipulation closes that gap: select the browser's live nodes, then read, change, create, or remove them. The planner starts at 0 of 3 complete | 0 points and updates as tasks are completed. Review the Complete JavaScript course first if selectors and event listeners are unfamiliar.

What the DOM is: turn HTML into a node tree

HTML is source text. The browser parses it into the Document Object Model, a live tree headed by document. Element nodes represent tags, text nodes hold text, and relationships describe position.

The browser parses the following HTML into a live node tree:

html
<section id="study-planner">
  <p id="summary">0 of 3 complete | 0 points</p>
  <ul id="task-list">
    <li class="task" data-id="dom-tree" data-points="20"><span>Map the DOM tree</span><button class="complete-btn">Complete</button></li>
    <li class="task" data-id="selectors" data-points="30"><span>Practise selectors</span><button class="complete-btn">Complete</button></li>
    <li class="task" data-id="mini-app" data-points="50"><span>Build the mini app</span><button class="complete-btn">Complete</button></li>
  </ul>
</section>

The section parents the paragraph and list. Its three list items are siblings. Each item contains a span, button, and text nodes.

DOM tree for the study planner: a section holding the summary paragraph and a task list of three items, each with a Complete button.

Select the right element before changing it

Selection comes before mutation. These calls return different shapes:

Selector

Return shape

Exact result

document.getElementById('summary')

One element or null

p#summary

document.querySelector('#task-list')

First match or null

ul#task-list

document.querySelector('[data-id="selectors"]')

First match or null

The 30-point task

document.querySelector('.task')

First match or null

The 20-point task

document.querySelectorAll('.task')

Static NodeList

Initial length is 3

Use [...document.querySelectorAll('.task')] for array methods such as filter and reduce. A static NodeList does not grow after an append. Single-element selectors can return null, so check before reading .textContent.

Change text, classes, attributes, and data values safely

This mutation marks the first task complete and updates its button and summary:

js
const firstTask = document.querySelector('.task');
firstTask.classList.add('done');
firstTask.dataset.complete = 'true';
firstTask.querySelector('button').textContent = 'Undo';
firstTask.querySelector('button').setAttribute('aria-pressed', 'true');
document.getElementById('summary').textContent = '1 of 3 complete | 20 points';

dataset.points returns the string '20', not the number 20. Convert it with Number(firstTask.dataset.points) before arithmetic. Use classList.toggle('done') for reversible state, setAttribute or removeAttribute for attributes, and a .done class for appearance.

For the literal input <img src=x onerror=alert(1)>, textContent displays characters as text. innerHTML parses markup, so never give it untrusted input. This planner uses textContent for labels.

Create, insert, replace, and remove elements

createTask(label, points, id) creates an li, sets className, dataset.id, and dataset.points, builds a span and button with textContent, then calls taskList.append(item). After createTask('Review event delegation', 40, 'event-delegation'), a fresh querySelectorAll('.task').length is 4.

For a notice before the list, create a paragraph containing Today's target: 140 points, then call document.getElementById('study-planner').prepend(notice). Replace with oldNode.replaceWith(newNode). Remove the added task with document.querySelector('[data-id="event-delegation"]').remove(); a fresh count returns to 3.

Sorting data and rendering nodes are separate jobs. If you later order by data-points, review Sorting Algorithms Compared without mixing sorting into the rendering helper.

Handle clicks with one delegated event listener

Attach one listener to #task-list. A click bubbles to the list: event.target is the button, while event.currentTarget remains ul#task-list. Delegation also catches buttons appended later.

The callback finds event.target.closest('.complete-btn') and returns without a button. It finds button.closest('.task'), toggles state, changes Complete to Undo, aligns aria-pressed, and calls updateSummary(). That function re-queries tasks, filters .task.done, converts with Number, reduces, and updates the summary.

Fully worked DOM manipulation example

Copy this into one HTML file and open it in a browser:

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Study planner</title>
  <style>.done span { text-decoration: line-through; opacity: .65; }</style>
</head>
<body>
  <section id="study-planner">
    <p id="summary">0 of 3 complete | 0 points</p>
    <ul id="task-list">
      <li class="task" data-id="dom-tree" data-points="20"><span>Map the DOM tree</span> <button class="complete-btn" aria-pressed="false">Complete</button></li>
      <li class="task" data-id="selectors" data-points="30"><span>Practise selectors</span> <button class="complete-btn" aria-pressed="false">Complete</button></li>
      <li class="task" data-id="mini-app" data-points="50"><span>Build the mini app</span> <button class="complete-btn" aria-pressed="false">Complete</button></li>
    </ul>
  </section>
  <button id="add-review">Add review task</button>
  <script>
    const taskList = document.getElementById('task-list');
    const summary = document.getElementById('summary');

    function updateSummary() {
      const tasks = [...document.querySelectorAll('.task')];
      const completed = tasks.filter(item => item.classList.contains('done'));
      const points = completed.reduce((sum, item) => sum + Number(item.dataset.points), 0);
      summary.textContent = `${completed.length} of ${tasks.length} complete | ${points} points`;
    }

    function createTask(label, points, id) {
      const item = document.createElement('li');
      item.className = 'task';
      item.dataset.id = id;
      item.dataset.points = points;
      const span = document.createElement('span');
      span.textContent = label;
      const button = document.createElement('button');
      button.className = 'complete-btn';
      button.textContent = 'Complete';
      button.setAttribute('aria-pressed', 'false');
      item.append(span, ' ', button);
      taskList.append(item);
    }

    taskList.addEventListener('click', event => {
      const button = event.target.closest('.complete-btn');
      if (!button) return;
      const item = button.closest('.task');
      if (!item) return;
      const done = item.classList.toggle('done');
      item.dataset.complete = String(done);
      button.textContent = done ? 'Undo' : 'Complete';
      button.setAttribute('aria-pressed', String(done));
      updateSummary();
    });

    document.getElementById('add-review').addEventListener('click', () => {
      if (!document.querySelector('[data-id="event-delegation"]')) {
        createTask('Review event delegation', 40, 'event-delegation');
        updateSummary();
      }
    });
  </script>
</body>
</html>

Trace the visible state, including every sum:

Action

Calculation

Output

Start

No completed tasks

0 of 3 complete | 0 points

Complete Map the DOM tree

0 + 20 = 20

1 of 3 complete | 20 points

Complete Practise selectors

20 + 30 = 50

2 of 3 complete | 50 points

Undo Map the DOM tree

50 - 20 = 30

1 of 3 complete | 30 points

Add and complete Review event delegation

30 + 40 = 70

2 of 4 complete | 70 points

Event delegation flow: a Complete-button click bubbles to the task list, toggles the task, sums its points, and updates the summary total.

Common DOM manipulation mistakes and a debugging checklist

Trap

Fix

Script runs before the HTML exists, producing null

Use defer, place the script after the HTML, or wait for DOMContentLoaded

querySelector is used when every match is needed

Use querySelectorAll

A static NodeList is expected to grow after append

Run a fresh query

Dataset strings are added directly

Convert each value with Number

Every button gets its own listener, so new buttons are missed

Delegate one listener from the list

Learner input is assigned to innerHTML

Assign it to textContent

Guard closest() before accessing its result. Debug in order: confirm the script loaded, log the selection, inspect target and currentTarget, check the class and data-complete in DevTools, then compare with the trace table.

How quizzes and interviews test DOM knowledge

Prediction questions expose your model of the live tree. With a listener on div#panel and a clicked button.save, event.target.className is save, while event.currentTarget.id is panel. Capture querySelectorAll('.task') at three tasks, append a fourth, and its length stays 3; a fresh query returns 4.

The broader JavaScript fresher interview guide connects DOM questions to scope, closures, async JavaScript, and the event loop. DOM-specific questions narrow the task to predicting selector results, mutation output, target versus currentTarget, static collections, and safe text updates.

The short version and what to learn next

Select, inspect, mutate, listen, then re-check the visible state. The five trace totals are 0, 20, 50, 30, and 70, each derived from the tasks currently marked done.

Use Complete JavaScript for the language sequence. Move to the MERN Stack course for full-stack projects, browse the Coding & Skills category for the wider inventory, or study Dynamic Programming Explained after the DOM mini app works.