This blog post is part of the series “Learning web development” – which teaches people who have never programmed before how to create web apps with JavaScript.
To download the examples, go to the GitHub repository learning-web-dev-code
and follow the instructions there.
I’m interested in feedback! If there is something you don’t understand, please write a comment at the end of this page.
In this chapter, we learn how to do things repeatedly in JavaScript.
for-of
loops Loops are JavaScript statements that execute the same piece of code zero or more times. The most popular loop is for-of
:
const arr = ['ready', 'set', 'go'];
for (const elem of arr) { // (A)
console.log(elem);
}
The code block (in curly braces) that starts at the end of line A is called the body of the loop.
This loop iterates over the array arr
:
arr[0]
to elem
and runs the body.arr[1]
to elem
and runs the body.One execution of the body is called a loop iteration.
This is what’s logged to the console:
ready
set
go
type.methodName()
In order to express “method .push()
of arrays”, two notations are common:
array.push()
Array.prototype.push()
We’ll use the first notation. The second one stems from the fact that Array.prototype
is where the methods are stored that we can invoke on arrays. How exactly that works is beyond the scope of this series, but you can read about it in section “The internals of classes” of the book “Exploring JavaScript”.
array.push()
The array method .push()
appends a value to the end of an array – thereby increasing its length by one:
> const arr = [];
> arr.push('a')
> arr
[ 'a' ]
> arr.push('b')
> arr
[ 'a', 'b' ]
array.push()
and a loop One common technique for processing an array is:
The following function is an example:
const extractNonEmptyStrings = (strs) => {
const result = [];
for (const str of strs) {
if (str.length > 0) {
result.push(str);
}
}
return result;
};
This is the function in action:
> extractNonEmptyStrings([''])
[]
> extractNonEmptyStrings(['yes', '', 'or', '', 'no'])
[ 'yes', 'or', 'no' ]
> extractNonEmptyStrings(['hello'])
[ 'hello' ]
array.join()
In the next project, we’ll need the array method .join()
. It creates a string by concatenating (combining) all the strings in an array into a single string. It puts the string provided as an argument between the elements.
> ['a', 'b', 'c'].join(', ')
'a, b, c'
> ['a', 'b', 'c'].join('-')
'a-b-c'
The argument of .join()
can also be an empty string:
> ['a', 'b', 'c'].join('')
'abc'
fruits-you-like-fixed.html
The user interface of this project works as follows:
The input comes from an unordered list with checkboxes for fruits (only one <li>
is shown below):
<ul>
<li>
<label>
<input type="checkbox" value="apples"> apples
</label>
</li>
<!-- Etc. -->
</ul>
The output goes to the following paragraph: text that lists the fruits that were selected.
<p>
You like: <span id="feedbackSpan"></span>
</p>
Ever time a checkbox is selected or deselected, the following function is called:
const updateFeedback = () => {
const selectedFruits = [];
for (const i of document.querySelectorAll('input')) {
if (i.checked) {
selectedFruits.push(i.value);
}
}
document.querySelector('#feedbackSpan').innerText =
selectedFruits.join(', ');
};
We first collect the names of all selected (“checked”) fruits and then show them in #feedbackSpan
, separated by commas.
How are we going to listen for changes made by the user to the checkboxes? One option is to add a 'change'
listener to each checkbox. However, there is an alternative: Each event is first sent to listeners at the source (the HTML element where they originate), then to listeners at the source’s parent (the immediately surrounding HTML element), then to listeners at the parent’s parent, etc. That is called event bubbling. If we want to stop an event from being passed on, we can call event.stopPropagation()
. But that is rarely necessary because we rarely have more than one event listener that listens to a given event (at different HTML elements).
In this project, we listen for the checkbox 'change'
events at the <ul>
element:
document.querySelector('ul')
.addEventListener(
'change',
(event) => {
updateFeedback();
}
);
In the project fruits-you-like-fixed.html
, the <ul>
contains a lot of repetitive HTML code. For the next project, we want to create that HTML via JavaScript, with the input being the following array:
const fruits = [
'apples',
'bananas',
'oranges',
'strawberries',
'watermelons',
];
In order to do that comfortably, we need to learn two new tools: The +=
operator and template literals.
+=
operator for numbers and strings The +=
is an abbreviation: The following two expressions are equivalent.
myVar += value;
myVar = myVar + value;
We can use this operator for numbers:
> let num = 0;
> num += 1;
> num
1
> num += 1;
> num
2
We can also use this operator for to concatenate strings:
> let str = '';
> str += 'ice ';
> str
'ice '
> str += 'cream';
> str
'ice cream'
+=
We have already used the array method .join()
. We can use +=
to implement this method ourselves:
const joinStrings = (strs, separator) => {
let result = '';
for (const [index, str] of strs.entries()) { // (A)
if (index > 0) {
result += separator;
}
result += str;
}
return result;
};
Let’s use this function:
> joinStrings(['a', 'b'], '-')
'a-b'
This for-of
loop in line A does something different: The array method strs.entries()
returns a data structure that is called an iterable. It is very loosely similar to an array. Its elements are index-value pairs. Each pair is stored in an array with two elements.
This is how array.entries()
works:
> Array.from(['a', 'b'].entries())
[ [ 0, 'a' ], [ 1, 'b' ] ]
Array.from()
converts its argument to an array.
const [index, str]
provides a different way of assigning to variables (so-called destructuring): The variables index
and str
get their data from inside a linear data structure such as an array. This is another example of destructuring:
> const [zero, one, two] = ['a', 'b', 'c'];
> zero
'a'
> one
'b'
> two
'c'
The following code shows the same loop, once with destructuring and once without it:
for (const [index, value] of ['yes', 'no', 'maybe'].entries()) {
console.log(index + '. ' + value);
}
console.log();
for (const pair of ['yes', 'no', 'maybe'].entries()) {
console.log(pair[0] + '. ' + pair[1]);
}
It should be obvious that the first loop – which uses destructuring – has more convenient syntax. The complete output is:
0. yes
1. no
2. maybe
0. yes
1. no
2. maybe
A template literal provides us with another way of creating strings that is similar to string literals. Where the content of string literals is delimited by single quotes or double quotes, the content of template literals is delimited by backticks:
const str = `This is a template literal!`;
Template literals have one feature that string literals don’t have: It lets us interpolate (insert) values:
> const num = 99;
> `${num} bottles of juice on the wall`
'99 bottles of juice on the wall'
const text = `First line
Second line
Third line`;
console.log(text);
First line
Second line
Third line
const words = ['eeny', 'meeny'];
let str = '';
for (const word of words) {
str += `
<p>
${word}
</p>
`;
}
str = str.replaceAll(' ', '·'); // (A)
str = str.replaceAll('\n', '⮐\n'); // (B)
console.log(str);
In this code we first assemble the string str
. Then we convert invisible characters to something we can see in the output. The string method .replaceAll()
helps us do that:
> 'baaab'.replaceAll('a', 'x')
'bxxxb'
What are we replacing?
\n
stands for newline, a character that ends the current line and is also called line break or line terminator. We don’t remove newlines, but we add visible symbols before them so that we can see them more clearly.The output of the code looks like this:
⮐
····<p>⮐
······eeny⮐
····</p>⮐
··⮐
····<p>⮐
······meeny⮐
····</p>⮐
··
That’s not ideal:
It’s clear why this happens: We want our template literal to look nice, which is why we added newlines and indented it.
The following code shows one way of producing better output:
const words = ['eeny', 'meeny'];
let str = '';
for (const word of words) {
str += `
<p>
${word}
</p>
`.trim();
str += '\n'; // (A)
}
str = str.replaceAll(' ', '·');
str = str.replaceAll('\n', '⮐\n');
console.log(str);
This code gives us the following nice output:
<p>⮐
··eeny⮐
</p>⮐
<p>⮐
··meeny⮐
</p>⮐
What’s different in this version?
We are not indenting the content of the template literal. The downside is that this looks a lot less nice.
We are using the string method .trim()
to remove all whitespace (spaces, tabs, newlines etc.) from the beginning and the end of the string created by the template literal.
> '\n abc \n '.trim()
'abc'
Because we removed the newlines at the beginning and the end of the template literal, we manually insert a newline after each template literal string (line A).
Another way of producing better output is via a library (code written by someone else that we can use) such as string-indent
. How exactly this library works is beyond the scope of this series; you can read more about a similar library in section “Multiline template literals and indentation” of the book “Exploring JavaScript”.
fruits-you-like-generated.html
fruits-you-like-generated.html
is almost like fruits-you-like-fixed.html
but its checkboxes are not created via fixed HTML. Its <ul>
is empty:
<ul>
</ul>
The <li>
elements are created via the following JavaScript code:
const fruits = [
'apples',
'bananas',
'oranges',
'strawberries',
'watermelons',
];
const ul = document.querySelector('ul');
for (const f of fruits) {
const html = `
<li>
<label>
<input type="checkbox" value="${f}"> ${f}
</label>
</li>
`;
ul.insertAdjacentHTML('beforeend', html); // (A)
}
In the previous section, we saw that writing the template literal assigned to html
like this produces a slightly ugly string. However, in this case, it doesn’t matter because HTML is very forgiving.
In line A, we used the method .insertAdjacentHTML()
of HTML elements to insert HTML code into the web page as if we had written it by hand. It is inserted before the end of the <ul>
.innerText
for status messages is dynamic HTML: The HTML changes dynamically, usually multiple times while we use an app.break
Consider the following functionality:
const firstStrStartingWithA = (strs) => {
for (const str of strs) {
if (str.startsWith('A')) {
return str; // (A)
}
}
return undefined;
};
In line A, we leave the loop as soon as we have found a suitable word. However, once we do that, we also leave the current function and the code after the loop is not executed.
break
lets us leave a loop and continue execution after the loop. As an example, the following equivalent implementation of firstStrStartingWithA()
uses break
:
const firstStrStartingWithA = (strs) => {
let result = undefined;
for (const str of strs) {
if (str.startsWith('A')) {
result = str;
break;
}
}
return result;
};
fruits-you-like-generated.html
so that it only allows one selection:
<input type="radio">
and make sure that the HTML doesn’t allow users to select more than one radio button (MDN explains how to do that).break
may help you here. Assign that value to a variable, don’t push it into an array. Benefit: When updating the feedback, you don’t need to .join()
an array.