Magyar zászlóMagyar

Task 2 – Interactive Video Table

JavaScript Exam 2024-25-2 15 points total

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 the data array. 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.


Populate the table
Array.map()innerHTMLtemplate literalsdata-* attributes
2 pts
Requirement

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();
Toggle row selection on click
event delegationclosest()classList.toggle()dataset
2 pts
Requirement

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 total views in #sum
Array.reduce()textContent
1 pt
Requirement

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;
}
Sum only selected rows (or all if none)
conditional expressionArray.filter()
2 pts
Requirement

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;
Add / subtract views for selected rows
parseFloat()querySelectorAll()Math.max()tr.cells
4 pts
Requirement

#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));
Sort table by column header click
Array.sort()localeCompare()bracket notationdata-sort
4 pts
Requirement

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();
});