Task 2 – Interactive Video Table
Project Files
The starter HTML has a pre-built table with two example rows, controls, and the #sum span. index.js has six DOM references. The data lives in data/data_array.js — add a <script> tag in index.html before your own script to load it:
<script src="../data/data_array.js"></script>
<script src="index.js"></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Task 2.</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<h1>2. Video list</h1>
<div id="main">
<span id="controls">
<input id="amount" placeholder="1.5"> million
<button id="btn-add">➕</button>
<button id="btn-sub">➖</button>
</span>
<span id="info">
Sum views: <span id="sum">85</span> million
</span>
<table id="video-table">
<thead>
<th data-sort="year">Year</th>
<th data-sort="title">Video title</th>
<th data-sort="views">Views</th>
</thead>
<tbody>
<!-- example rows — your renderTable() will replace these -->
<tr>
<td>2023</td>
<td>Käärijä - Cha Cha Cha</td>
<td>44 million</td>
</tr>
<tr>
<td>2024</td>
<td>Baby Lasagna - Rim Tim Tagi Dim</td>
<td>41 million</td>
</tr>
</tbody>
</table>
</div>
<!-- ✏️ Add <script src="../data/data_array.js"></script> here -->
<script src="index.js"></script>
</body>
</html>// Starter file — 6 DOM references provided, write your solution below
const videoTable = document.querySelector('#video-table')
const videoTableBody = document.querySelector('#video-table tbody')
const viewInput = document.querySelector('#amount')
const btnAdd = document.querySelector('#btn-add')
const btnSub = document.querySelector('#btn-sub')
const sumSpan = document.querySelector('#sum')data is an array of 32 video objects: { id, yt, title, year, views, nation } where views is in millions.
Architecture
This task has six interdependent subtasks. The cleanest approach uses two helper functions called whenever state changes:
renderTable()— rebuilds the<tbody>from thedataarray. Used on load and after sorting.updateSum()— reads selection state from data and writes the correct value to#sum. Called after every change.
Each <tr> gets a data-id="${video.id}" attribute that links it back to its data object so event handlers can find the matching object by ID instead of by index.
List the videos into #video-table following the pattern (year, title, views in millions).
map().join('') turns the data array into one HTML string. join('') uses an empty separator so there are no gaps between rows. Setting innerHTML replaces the placeholder rows from the HTML.
The data-id attribute is not required by subtask a, but adding it now is essential groundwork. Every subsequent subtask (b, e, f) needs to link a clicked <tr> back to its data object — tr.dataset.id makes that a one-liner.
renderTable() calls updateSum() at the end so the displayed sum is always correct immediately after a re-render (important for subtask f after sorting).
function renderTable() {
videoTableBody.innerHTML = data
.map(video => `
<tr data-id="${video.id}">
<td>${video.year}</td>
<td>${video.title}</td>
<td>${video.views} million</td>
</tr>
`)
.join('');
updateSum();
}
renderTable(); When a row is clicked, apply the selected class to it. If already selected, remove it on click.
Event delegation on <tbody> catches clicks on any row — including rows added later by sorting — with a single listener. Attaching a listener to each <tr> individually would break after renderTable() replaces the DOM.
event.target.closest('tr') is needed because the click might land on a <td> (or deeper). closest() walks up to the nearest ancestor matching the selector. If the click misses all rows, it returns null and the early return prevents errors.
classList.toggle('selected') adds the class if absent, removes it if present — one line for both directions.
Mirroring the state onto video.selected keeps the data array in sync with the DOM. This lets updateSum() read selection from data without querying the DOM — which is faster and avoids re-selecting DOM elements in every event handler.
videoTableBody.addEventListener('click', (event) => {
const tr = event.target.closest('tr');
if (!tr) return;
tr.classList.toggle('selected');
const video = data.find(v => v.id === tr.dataset.id);
if (video) video.selected = tr.classList.contains('selected');
updateSum();
}); Display the total number of views of all videos in the #sum element.
Writing updateSum() to handle both c and d at once avoids a rewrite when subtask d arrives. The logic: “sum the selected videos; if none are selected, sum all” — which is exactly what subtask d requires.
Math.round(total * 100) / 100 prevents floating-point display artifacts like 295.00000000003. Multiply by 100, round to the nearest integer, divide back by 100 — this gives at most 2 decimal places without changing the value.
reduce() accumulates a running total starting from 0 (the second argument). Without the 0, reduce would use the first array element as the initial value, which is an object — adding an object to a number gives NaN.
function updateSum() {
const selected = data.filter(v => v.selected);
const toSum = selected.length > 0 ? selected : data;
const total = toSum.reduce((sum, v) => sum + v.views, 0);
sumSpan.textContent = Math.round(total * 100) / 100;
} If any video is selected, #sum should show only the sum of selected videos’ views. If none are selected, show the overall sum.
data.filter(v => v.selected) returns an empty array [] when nothing is selected. [].length > 0 is false, so the ternary falls back to data — the full array.
This is the standard pattern for “use selection if any, else use all”: check the filtered array’s length before branching. One ternary covers both cases.
The updateSum() function written for subtask c already handles this. No new code needed.
The key lines:
const selected = data.filter(v => v.selected);
const toSum = selected.length > 0 ? selected : data; #btn-add increases the views of all selected rows by the value in #amount; #btn-sub decreases it, never below zero. If nothing is selected, do nothing. Keep #sum up to date.
A shared adjustViews(delta) helper with +1 / -1 eliminates duplicate code for add and subtract. Multiplying delta * amount means the same function handles both directions.
parseFloat(viewInput.value) reads the input field. If the user typed text, parseFloat returns NaN — the isNaN() guard exits early before any changes happen.
Math.max(0, video.views + delta * amount) handles “never below zero” in one expression. No if statement needed.
Why update tr.cells[2] instead of calling renderTable()? Re-rendering the whole table would wipe the CSS selected class from all rows, losing the user’s selection. Patching only the views cell (index 2) keeps everything else intact. Updating video.views in the data object ensures the data and DOM stay in sync for updateSum().
function adjustViews(delta) {
const amount = parseFloat(viewInput.value);
if (isNaN(amount)) return;
const selectedRows = videoTableBody.querySelectorAll('tr.selected');
if (selectedRows.length === 0) return;
selectedRows.forEach(tr => {
const video = data.find(v => v.id === tr.dataset.id);
if (!video) return;
video.views = Math.max(0, video.views + delta * amount);
tr.cells[2].textContent = `${Math.round(video.views * 100) / 100} million`;
});
updateSum();
}
btnAdd.addEventListener('click', () => adjustViews(+1));
btnSub.addEventListener('click', () => adjustViews(-1)); Clicking header cells should sort the table in descending order based on the clicked column. No need to preserve selection after sorting. No need to toggle direction on repeated clicks.
closest('th[data-sort]') finds the clicked header and its data-sort value ("year", "title", or "views"). These values exactly match the property names on data objects, making bracket notation b[key] - a[key] work for both year and views without separate if branches.
Descending sort:
- Numbers:
b[key] - a[key]→ largest value first (e.g., newest year, most views) - Strings:
b.title.localeCompare(a.title)→ Z to A, which the task explicitly accepts
Resetting v.selected = false before calling renderTable() is required. If any row was selected, updateSum() (called inside renderTable()) would otherwise still compute the “selected sum” even though no rows appear selected after re-rendering.
videoTable.querySelector('thead').addEventListener('click', (event) => {
const th = event.target.closest('th[data-sort]');
if (!th) return;
const key = th.dataset.sort; // "year", "title", or "views"
data.sort((a, b) => {
if (key === 'title') return b.title.localeCompare(a.title);
return b[key] - a[key];
});
data.forEach(v => v.selected = false);
renderTable();
}); Complete Solution
// Assumes `data` is an array copied from data/data_array.js
// Each item: { id, yt, title, year, views, nation }
function updateSum() {
const selected = data.filter(v => v.selected);
const toSum = selected.length > 0 ? selected : data;
const total = toSum.reduce((sum, v) => sum + v.views, 0);
sumSpan.textContent = Math.round(total * 100) / 100;
}
function renderTable() {
videoTableBody.innerHTML = data
.map(video => `
<tr data-id="${video.id}">
<td>${video.year}</td>
<td>${video.title}</td>
<td>${video.views} million</td>
</tr>
`)
.join('');
updateSum();
}
renderTable();
// b: row selection toggle
videoTableBody.addEventListener('click', (event) => {
const tr = event.target.closest('tr');
if (!tr) return;
tr.classList.toggle('selected');
const video = data.find(v => v.id === tr.dataset.id);
if (video) video.selected = tr.classList.contains('selected');
updateSum();
});
// e: add / subtract views
function adjustViews(delta) {
const amount = parseFloat(viewInput.value);
if (isNaN(amount)) return;
const selectedRows = videoTableBody.querySelectorAll('tr.selected');
if (selectedRows.length === 0) return;
selectedRows.forEach(tr => {
const video = data.find(v => v.id === tr.dataset.id);
if (!video) return;
video.views = Math.max(0, video.views + delta * amount);
tr.cells[2].textContent = `${Math.round(video.views * 100) / 100} million`;
});
updateSum();
}
btnAdd.addEventListener('click', () => adjustViews(+1));
btnSub.addEventListener('click', () => adjustViews(-1));
// f: header sort
videoTable.querySelector('thead').addEventListener('click', (event) => {
const th = event.target.closest('th[data-sort]');
if (!th) return;
const key = th.dataset.sort;
data.sort((a, b) => {
if (key === 'title') return b.title.localeCompare(a.title);
return b[key] - a[key];
});
data.forEach(v => v.selected = false);
renderTable();
});