Ugrás a tartalomra

2. gyakorlat - Órai feladatok

webprogramming language-features arrays objects dom events in-class

Today has two parts. In the first part (tasks a to g) we continue with arrays: this time the arrays contain objects, and we meet a few new array methods. In the second part (tasks h to l) we finally leave the Console and start changing the page itself with JavaScript.

You work on your own today. Each task starts with a short explanation whenever it needs something new, so read the task text before you start typing. The tasks get harder as you go. Whatever you do not finish in class, finish at home.

How to run it

Download the starter files, open the folder in VS Code, then right-click index.html and choose Open with Live Server. Press F12 and open the Console tab.

The starter has three files:

  • index.html: the page. You do not need to change it.
  • arrays.js: tasks a to g. The results go to the Console.
  • dom.js: tasks h to l. The results appear on the page.

Look at the bottom of index.html:

<script src="arrays.js"></script>
<script src="dom.js"></script>

Two things matter here. First, the scripts are at the end of the body, so by the time they run, every element of the page already exists. Second, the order matters: arrays.js runs first, so dom.js can use products and every function you write in arrays.js.

How to check your work

Same as last week: write the function, remove the // from the console.log line under it, save, and compare the Console with the expected output in the task. If you see undefined, you probably forgot a return.

Objects

Last week our arrays held numbers. An object stores several values together, each under a name. These named values are called properties.

const product = { name: "Keyboard", price: 25, stock: 12 };

console.log(product.name);    // "Keyboard"
console.log(product.price);   // 25

product.price = 20;           // change a property
product.color = "black";      // add a new property

A property can hold anything, even an array:

const product = { name: "Mouse", ratings: [3, 4] };

console.log(product.ratings[0]);   // 3

Arrays of objects

In arrays.js you will find a products array. Every element is an object with the same shape:

{ name: "Keyboard", price: 25, category: "electronics", stock: 12, ratings: [5, 4, 4] }

The array methods you already know work exactly the same way. The only difference is that the callback receives a whole object, so you reach for its properties inside:

// numbers:  numbers.filter((n) => n > 10)
// objects:  products.filter((product) => product.price > 10)

Array methods at a glance

You do not need to memorize this table. Come back to it whenever a task needs something.

MethodThe callback returnsThe method returns
mapa new valuea new array, same length
filtertrue or falsea new array with the elements that got true
findtrue or falsethe first element that got true, or undefined
sometrue or falsetrue if at least one element got true
everytrue or falsetrue if all elements got true
reducethe new accumulated valueone single value
sorta number (negative, zero, positive)the same array, sorted in place

A few more that need no callback: includes(value) (is it in the array?), join(separator) (glue elements into a string), flat() (unpack nested arrays), slice(start, end) (copy a part).

Kezdőcsomag

A feladatleírás és a kezdőfájlok helyi munkához előkészítve.

Tartalmazott fájlok: arrays.jsdom.jsindex.html

Feladatok

inStockNames(products)

Part 1: arrays of objects. Tasks a to g go into arrays.js.

Collects the names of the products that can be bought right now. Chain filter and map, just like cubeEvens last week, but reach into the objects.

  • Parameter: products: an array of product objects.
  • Returns: a new array with the name of every product whose stock is greater than 0, in the original order.
console.log(inStockNames(products));
console.log(inStockNames([]));

Should print:

["Keyboard", "Monitor", "Notebook", "Backpack"]
[]

findProduct(products, name)

Looks up one product by its name.

New: find. filter always returns an array, even when you only want one element. find returns the first matching element itself, not an array. If nothing matches, it returns undefined.

const numbers = [4, 9, 12, 20];
console.log(numbers.find((n) => n > 10));    // 12
console.log(numbers.find((n) => n > 100));   // undefined
  • Parameters: products: an array of product objects; name: a string.
  • Returns: the product object whose name is exactly name. If there is no such product, return null (not undefined).
console.log(findProduct(products, "Monitor"));
console.log(findProduct(products, "Laptop"));

Should print:

{ name: "Monitor", price: 180, category: "electronics", stock: 4, ratings: [5, 5, 4, 5] }
null

hasCheaperThan(products, limit) and allInStock(products)

Two short functions that answer a yes or no question about the whole array.

New: some and every. Both take a callback that returns true or false, and both return a single boolean.

  • some asks: is there at least one element for which the callback is true?
  • every asks: is the callback true for all elements?
const numbers = [4, 9, 12];
console.log(numbers.some((n) => n > 10));    // true, 12 is bigger
console.log(numbers.every((n) => n > 10));   // false, 4 and 9 are not

hasCheaperThan(products, limit)

  • Parameters: products: an array of product objects; limit: a number.
  • Returns: true if at least one product has a price strictly less than limit, otherwise false.

allInStock(products)

  • Parameter: products: an array of product objects.
  • Returns: true if every product has a stock greater than 0, otherwise false.
console.log(hasCheaperThan(products, 5));
console.log(hasCheaperThan(products, 1));
console.log(allInStock(products));

Should print:

true
false
false

priceRange(products)

Finds the cheapest and the most expensive price.

New: Math.min, Math.max and spreading into a call. Math.min and Math.max take separate numbers, not an array:

console.log(Math.max(3, 8, 2));      // 8
console.log(Math.max([3, 8, 2]));    // NaN, an array is not a number

Last week you used ... to unpack an array inside [ ]. It works inside the parentheses of a function call too, turning the array into separate arguments:

const values = [3, 8, 2];
console.log(Math.max(...values));    // 8, same as Math.max(3, 8, 2)

So first collect the prices into an array of numbers (which method turns objects into numbers?), then spread them.

  • Parameter: products: a non-empty array of product objects.
  • Returns: an object with two properties: min, the lowest price, and max, the highest price.
console.log(priceRange(products));

Should print:

{ min: 1, max: 180 }

Try Math.max() with no arguments at all in the Console. The result (-Infinity) is the reason this task promises a non-empty array.

totalStockValue(products)

Calculates how much all the products in the warehouse are worth together.

New: reduce. reduce walks the array and boils it down into one value. You already wrote this by hand in sumAll last week:

let sum = 0;                 // start value
for (const n of numbers) {
  sum = sum + n;             // new sum = old sum + current element
}

With reduce, the same thing is:

const sum = numbers.reduce((sum, n) => sum + n, 0);

The callback gets two parameters: the value accumulated so far and the current element. It returns the new accumulated value. The second argument of reduce (here 0) is the start value. Always give one: without it, reduce crashes on an empty array.

  • Parameter: products: an array of product objects.
  • Returns: the sum of price * stock over all products. Returns 0 for an empty array.
console.log(totalStockValue(products));
console.log(totalStockValue([]));

Should print:

1450
0

sortedByPrice(products)

Orders the products from cheapest to most expensive.

New: sort with a compare function. sort calls your function with two elements, a and b, and looks at the number you return:

  • negative: a comes first
  • positive: b comes first
  • zero: keep their order

For numbers, a - b gives exactly that, and sorts in ascending order:

const numbers = [30, 4, 100];
numbers.sort((a, b) => a - b);
console.log(numbers);   // [4, 30, 100]

Two warnings:

  1. Without a compare function, sort compares elements as strings: [30, 4, 100].sort() gives [100, 30, 4]. Always pass one.
  2. sort does not create a new array. It rearranges the original one. To keep the original untouched, copy it first with spread: [...numbers].sort(...).
  • Parameter: products: an array of product objects.
  • Returns: a new array with the same product objects, ordered by price from lowest to highest. The input array must stay in its original order.
console.log(sortedByPrice(products).map((product) => product.name));
console.log(products[0].name);

Should print:

["Pen", "Notebook", "Mouse", "Keyboard", "Backpack", "Monitor"]
Keyboard

If the second line prints Pen, you sorted the original array.

matrixMax(matrix) and allRatings(products)

Two functions that deal with arrays inside arrays.

New: flat. A matrix in JavaScript is simply an array of arrays (the rows):

const grid = [
  [3, 8, 1],
  [9, 2, 7],
  [4, 6, 5],
];

console.log(grid[1][0]);   // 9, row 1, column 0

flat() removes one level of nesting, so the rows are unpacked into one long array. After that, every method you know works on the matrix:

console.log([[1, 2], [3], [4, 5]].flat());   // [1, 2, 3, 4, 5]

matrixMax(matrix)

  • Parameter: matrix: a non-empty array of non-empty arrays of numbers.
  • Returns: the largest number anywhere in the matrix.

allRatings(products)

  • Parameter: products: an array of product objects.
  • Returns: a new array with every rating of every product, in order. Hint: map each product to its ratings array first. What do you get? What does flat do with it?
console.log(matrixMax(grid));
console.log(matrixMax([[-4, -2], [-9]]));
console.log(allRatings(products));

Should print:

9
-2
[5, 4, 4, 3, 4, 5, 5, 4, 5, 4, 2, 3, 5]

Change the heading

Part 2: the DOM. Tasks h to l go into dom.js, and you check them by looking at the page.

The browser turns the HTML into a tree of objects called the DOM (Document Object Model). JavaScript can find these objects and change them, and the page updates immediately.

New: selecting an element. document.querySelector takes a CSS selector (the same kind you write in a stylesheet) and returns the first matching element. If nothing matches, it returns null.

const heading = document.querySelector("h1");        // by tag name
const box = document.querySelector("#subtitle");     // by id, with #

New: changing text, HTML and style.

heading.innerText = "Hello <i>world</i>";   // shows the tags literally: Hello <i>world</i>
heading.innerHTML = "Hello <i>world</i>";   // renders them: Hello world, in italics

heading.style.color = "red";
heading.style.backgroundColor = "yellow";   // CSS background-color becomes backgroundColor

In JavaScript, CSS property names with a hyphen are written in camelCase: background-color becomes backgroundColor, font-size becomes fontSize.

Your job:

  1. Select the element with the id title. Change its text to Practice 2 - DOM, its text color to white and its background color to darkslateblue.
  2. Select the element with the id subtitle. Set its content to This page is now <strong>controlled by JavaScript</strong>. so that the middle part really appears in bold.

Expected result: a white heading on a dark purple background, with a partly bold line under it.

If the Console says Cannot set properties of null, querySelector found nothing: check the spelling of the selector, and do not forget the #.

Show the product list

Fills the product section of the page using the array function from task a.

  1. Select #product-count and set its text to 4 of 6 products are in stock. Do not type the numbers in: calculate them with inStockNames(products).length and products.length, and build the sentence with a template literal.
  2. Select #product-list (an empty <ul>). Put one <li> into it for every name returned by inStockNames(products).

New: building HTML from an array. innerHTML expects one string. map turns each name into a string like "<li>Keyboard</li>", then join("") glues the pieces together with nothing between them:

const colors = ["red", "blue"];
console.log(colors.map((c) => `<li>${c}</li>`).join(""));
// "<li>red</li><li>blue</li>"
  1. Make every <li> inside #product-list green.

New: selecting many elements. document.querySelectorAll returns all matching elements in a list (a NodeList). If nothing matches, the list is empty. You can walk it with for...of:

const paragraphs = document.querySelectorAll("p");
for (const p of paragraphs) {
  p.style.fontWeight = "bold";
}

Expected result: the text 4 of 6 products are in stock, and a green list with Keyboard, Monitor, Notebook and Backpack.

Click counter

When the Increase button is clicked, the number next to it goes up by one.

New: event handling. Until now, all our code ran once, when the page loaded. To react to the user, you tell the browser: when this happens to this element, run this function. Every event handling task follows the same three steps:

// Step 1: select the elements you need
const button = document.querySelector("#some-button");

// Step 2: write the function that does the work (the event handler)
function handleButtonClick() {
  console.log("clicked");
}

// Step 3: connect them (the event listener)
button.addEventListener("click", handleButtonClick);

Note that in step 3 you write handleButtonClick without parentheses. You are handing over the function for the browser to call later. With handleButtonClick() you would call it immediately, once, and hand over its result.

Your job: the counter is the #counter span, the button is #increase. In the handler, read the current number from the span, add one, and write it back.

innerText is always a string. "0" + 1 gives "01", so convert it first with Number(...), like in Practice 1.

Expected result: clicking the button shows 1, 2, 3, and so on.

Price filter

The user types a maximum price and clicks Filter. The list below shows every product whose price is at most that amount, in the form Keyboard (25 EUR).

  • Input field: #max-price, button: #filter-button, list: #filtered-list.
  • If no product matches, the list should contain a single item: No products found.
  • Every click replaces the previous result, it does not add to it.

New: reading an input field. The text inside an <input> is in its value property, not in innerText. Like innerText, it is always a string:

const input = document.querySelector("#some-input");
const typed = input.value;          // "25", a string
const amount = Number(input.value); // 25, a number

Read the value inside the event handler. If you read it at the top of the file, you get whatever was in the field when the page loaded: an empty string.

This task combines everything from today: an event, input.value, filter, map, join and innerHTML.

Expected results:

  • max price 20: Mouse (15 EUR), Notebook (3 EUR), Pen (1 EUR) (in the order of the products array)
  • max price 0: No products found

Guess the number

Challenge task. A small game: when the page loads, the computer picks a secret whole number between 1 and 100. The user types a guess into #guess and clicks #guess-button. #guess-result then shows:

  • Higher if the secret number is bigger than the guess
  • Lower if the secret number is smaller than the guess
  • Correct if the guess is right

New: random numbers. Math.random() returns a random decimal number from 0 (included) to 1 (not included), for example 0.4271. To get a whole number from 1 to 100:

Math.random() * 100                  // 0 to 99.999...
Math.floor(Math.random() * 100)      // 0 to 99, Math.floor cuts off the decimals
Math.floor(Math.random() * 100) + 1  // 1 to 100

Think about where the secret number is created. If you create it inside the event handler, the computer picks a new number on every click, and the game can never be won.

While testing, it helps to console.log the secret number once so you know what to type.

Extra: count the guesses and show them too, for example Correct, 6 guesses.

Követelmények

  • Write tasks a to g as function declarations, in the form function name(...) { ... }.
  • Declare variables with const or let. Do not use var.
  • A function that returns an array must return a new array and leave its input unchanged. This includes sortedByPrice.
  • Use strict comparison (=== and !==), not == and !=.
  • Do not change index.html. Select elements by the ids given in the tasks.
  • Attach event handlers with addEventListener. Do not use onclick attributes in the HTML.