Apress.JavaScript.for.Web.Developers
Apress.JavaScript.for.Web.Developers
Web Developers
Understanding the Basics
—
Mark Simon
JavaScript for Web
Developers
Understanding the Basics
Mark Simon
JavaScript for Web Developers: Understanding the Basics
Mark Simon
Ivanhoe, VIC, Australia
Introduction���������������������������������������������������������������������������������������xix
v
Table of Contents
vi
Table of Contents
vii
Table of Contents
viii
Table of Contents
ix
Table of Contents
x
Table of Contents
Toggling Properties��������������������������������������������������������������������������������������246
Transitions���������������������������������������������������������������������������������������������������247
Creating Style Sheets����������������������������������������������������������������������������������247
Coming Up���������������������������������������������������������������������������������������������������������247
xi
Table of Contents
xii
Table of Contents
Timing����������������������������������������������������������������������������������������������������������333
Using the Keyboard�������������������������������������������������������������������������������������334
Refactoring��������������������������������������������������������������������������������������������������334
Parent, Child, and Sibling Elements�������������������������������������������������������������334
Cross Fade���������������������������������������������������������������������������������������������������334
Coming Up���������������������������������������������������������������������������������������������������������335
xiii
Table of Contents
Index�������������������������������������������������������������������������������������������������379
xiv
About the Author
Mark Simon has been involved in training and
education since the beginning of his career.
He started as a teacher of mathematics but
soon moved into IT consultancy and training
because computers are much easier to work
with than high school students! He has worked
with and trained in several programming and
coding languages and currently focuses on web
development and database languages. When
not involved in work, you will generally find
Mark listening to or playing music, reading, or
just wandering about.
xv
About the Technical Reviewer
Jeff Friesen is a freelance software developer
and educator conversant in multiple operating
systems, programming languages, and other
technologies. His specialty is Java Standard
Edition.
He is currently exploring bare metal
programming for the Raspberry Pi and
also exploring machine learning. He’s also
developing his own programming languages
and tools (such as compilers and a news
aggregator) that facilitate his life.
xvii
Introduction
For the impatient:
If you’re anxious to get going, you can ignore all of this and skip over
to the Setup section. It’s very important and will make the rest of the
book much easier to work with.
When you load a web page, it’s quite possible that you’ll see the result of
up to five technologies. Two of these will have occurred at the web server,
and three will be happening within the browser itself.
On the server, the page and its components may well have been
generated by software. Typically, though not necessarily, that software may
be something like PHP, which is a scripting language. In turn, the software
may access a database to get some of its data using a database language
called SQL.
The eventual output will be in the form of HTML. That HTML will be
sent back to the browser.
The browser will then begin processing the HTML. If there’s a
reference to additional images, the browser will request these and use
them when they arrive. The HTML is also likely to include references
to CSS and JavaScript files. The three important browser technologies
involved are as follows:
xix
Introduction
xx
Introduction
What happens next is, of course, up to you. With what you’ll learn in
this book, you’ll be ready to create your own interactive web pages and to
rework any web projects which you may have inherited.
1
Microsoft also decided to add VBScript to Internet Explorer under the impression
the developers actually wanted to write code in Visual Basic. That’s all in the
past now.
xxi
Introduction
In some cases, you’ll write some code that you can save in a code
library to reuse in other projects.
xxii
Introduction
Older Methods
There have been many improvements to the way JavaScript works,
especially over recent years. However, each browser vendor will
implement these improvements in their own time, and each user will
update their browser in their own time. As a result, writing JavaScript for
the Web is a balance of using techniques that are as modern as possible,
without at the same time leaving too many users behind.
Thankfully, modern browsers have gone past the sorts of major
discrepancies that have been the bane of web developers for so many
years. They’re all more or less compatible with the latest mainstream web
technologies, and they’re mostly on the path to acceptable standards.
In this book, all of the JavaScript code will work on all the major
browsers, even if those browsers are a few years old. If you want to check
on a particular feature, you can check one of the following sites:
xxiii
Introduction
Setup
If you’ve planned a holiday near the beach somewhere, you may decide to
take this book with you for a little light reading. Hopefully it’s not too hard
to read.
However, you’ll get more from this book if you follow along with the
worked samples. To get the most out of this book, you’ll need
xxiv
Introduction
A Coding Editor
Apart from images, most of the files for a website, especially the sample
site here, are text files. That means you can edit these files with any old text
editor, even Windows Notepad, if that’s your idea of having a good time.
More realistically, you’ll want to use a text editor designed for writing
code. Here are the features you’ll want:
xxv
Introduction
You may already have your preferred coding editor. If you don’t, you
can try one of these:
• Pulsar: https://pulsar-edit.dev/
• VSCodium: https://vscodium.com/
Both are free, cross-platform, and open source. They also have
additional extension packages available to customize your editor.
xxvi
Introduction
Macintosh Windows
To set it up:
4. Start.
When the server is started, you’ll see a link to the site, something like
http://localhost:8000
You can click on it to load the page in your browser.
xxvii
Introduction
xxviii
CHAPTER 1
Working with
JavaScript
If you’re completely new to JavaScript, or to programming altogether, there
are some basic concepts that you’ll need to know. Many of these concepts
are common to other programming languages, but every language has its
own twists and quirks.
In this chapter, we’ll have an introductory look at the following ideas:
• Functions
• Adding comments to your code
• Special values
Even if you’re familiar with these concepts from another language, it’s
worth glancing over this chapter in case things are significantly different
from your own experience.
You’ll then have a look at setting up a basic outline for adding
JavaScript to your web pages.
OS Keys Meaning
MacOS option-command-I
Windows control-shift-I
2
Chapter 1 Working with JavaScript
In the figure, the developer toolbox has been docked to the right.
You don’t need to have this particular page open: any page will do.
Later, however, you’ll use the console to work with specific pages.
When using the console, you’ll notice a few peculiarities:
Although we’ll be using the console interactively, you can also write to
the console from your JavaScript. You wouldn’t normally see the output, of
course, but you can open up the console while you’re troubleshooting.
3
Chapter 1 Working with JavaScript
» 3+5
If you press enter, you’ll see the evaluated result after a sort of
left arrow:
» 3+5
← 8
» console.log('Hello');
Hello
← undefined
You’re actually seeing two results here. The first is the output of the
console.log() command: Hello, as you would expect. The second is the
value of the statement, which, for technical reasons, is a special value called
undefined. If you think that’s confusing, well, yes, it is. The good news is
that you can ignore it.
In the samples, you’ll sometimes also see some additional text after a
double-slash (//):
We’ll say more on that later, but that text is called a comment and is
ignored by JavaScript. It’s only there for the human reading the code.
4
Chapter 1 Working with JavaScript
Much of the rest of the book deals with real code which you’ll write and
save in their own documents. However, from time to time, we’ll need the
console again to experiment with a few ideas. As a reminder, we’ll show the
prompt character when we do.
For the rest of this chapter, you can press enter after each sample,
unless you’re asked not to. In some cases, we’ll show you what the output
should look like, but mostly we’ll leave that to you.
5
Chapter 1 Working with JavaScript
• Calculation
• Functions
• Control structures such as testing and repetition
• JavaScript object
In this chapter, we’ll cover the first few. We’ll look at the others as we
start writing more code in later chapters.
Before we start, we’ll need to clear up some terminology. JavaScript,
like most other programming languages, is very fussy about what you type,
so to avoid ambiguity, we’ll need to know the correct names for some of
the characters:
6
Chapter 1 Working with JavaScript
You’ll learn more about using these characters later. This is just to
make sure we know what they’re called.
Statements
JavaScript is made up of a number of statements and statement blocks.
There are different types of statements.
The simplest type of JavaScript statement simply calls some predefined
block of code:
» console.log('Hello');
7
Chapter 1 Working with JavaScript
Statement Blocks
There are times when you need to put multiple statements in a block.
Usually it’s so that a group of statements can be run or ignored together,
possibly after a test. Sometimes, it’s just to isolate the statements from the
rest of the code.
A block of statements is wrapped inside braces. For example:
» {
console.log('Hello');
console.log('Goodbye');
}
To get the extra lines in the console log, you may need to use .
In this case, grouping the statements has no real effect. Later, it will be
an important technique.
You will have noticed that in the code sample, the statements inside
the block are indented. JavaScript doesn’t care whether the code is
indented or not, but the indentation makes it easier to see how the blocks
are structured. All of the code in the book will be clearly indented, and it’s
strongly recommended that you do the same to make your code easier to
read and maintain.
Expressions
An expression is a combination of terms that results in a single value.
Examples of expressions are
1 + 2
12 / 3
5
8
Chapter 1 Working with JavaScript
» 3 + 5
← 8
» a = 12/3;
» console.log(a + 5);
9
← undefined
Spacing
You don’t normally have to include spaces in your expression. For
example:
» console.log(1+2+3);
» console.log(1 + 2 + 3); //Same thing
» console.log(1--2); // error
» console.log(1 - -2); // 1 minus negative 2
3
9
Chapter 1 Working with JavaScript
Semicolons
There is a lot of argument about the use of semicolons in learned
circles! In this book, we recommend them.
» a = 3;
» console.log(a + 4);
» a = 3
» console.log(a + 4)
They can normally be guessed if they would have occurred at the end
of the line. This guessing is referred to as automatic semicolon insertion.
Semicolons cannot be guessed if the semicolon is expected to occur in
the middle of the line:
In this case, you have no choice but to separate the statements with a
semicolon:
In this book, all code will include semicolons at the end of every
statement.
10
Chapter 1 Working with JavaScript
[variable] = [expression]
This places the value on the right of the equals sign into the variable
named on the left. For example:
» a = 3;
» a = 0;
» a = 3;
The first assignment puts 0 into a. The second statement does not state
whether a is 3; just that the new value is 3.
11
Chapter 1 Working with JavaScript
» a = 3;
» a = a + 1; // New value of a gets old value + 1
» console.log(a);
This statement is best read as “the new value of a is the old value of a
plus 1”. Nobody ever says it this way, but we should at least remember that
it’s what it means.
12
Chapter 1 Working with JavaScript
Chaining Assignments
You can chain assignments together in a single statement. A statement
such as the following is common:
» a = b = c = 0;
» console.log(a);
» console.log(b); // etc
Reading from the right, this will put 0 into c, which then goes into b,
which then goes into a.
Assignment Operators
JavaScript also has combined arithmetic assignment operators. These
allow you to perform an operation with the old value of a variable and
assign it as the new value. They are, in fact, simply an abbreviation for
separate operators.
For example:
» value = 2;
» value = value+3;
» value = 2;
» value += 3;
13
Chapter 1 Working with JavaScript
» a = 5;
» a += 10;
» console.log(a);
Operator Meaning
» a = a+1;
» a += 1;
» a++;
14
Chapter 1 Working with JavaScript
The other major rule when naming JavaScript variables is that you
shouldn’t use names that are reserved as part of the JavaScript language.
This does not rule out names of preexisting functions or variables, which
you are at liberty of replacing (sometimes at your peril).
15
Chapter 1 Working with JavaScript
Case Sensitivity
JavaScript variables, together with the rest of the JavaScript language, are
case sensitive. This means that you cannot define them in one case and
use them in another. For example:
» thing = 'Hello';
» console.log(Thing); // oops
Declaring Variables
It’s one thing to use and assign variables. JavaScript first has to become
aware of the variables you’re going to use so that it can prepare storage for
them. When you prepare for a variable, you are declaring the variable.
In JavaScript, there are four ways to declare a variable:
» let a;
» a = 3;
16
Chapter 1 Working with JavaScript
» const a = 3;
» a = 4; // error
Notice that the const statement assigns the value at the same time. You
have to do it this way, since a const can’t be assigned later.
You can also do this with var and let:
» var a = 3;
» let a = 3;
That last method is what we’ve been doing all along so far, but it’s
going to have to stop now. We’ll see why shortly.
You can declare multiple variables with a single statement. For
example:
» var a = 3, b = 4;
17
Chapter 1 Working with JavaScript
When we write our JavaScript more seriously, we’ll use let or const
as needed.
18
Chapter 1 Working with JavaScript
Strict Mode
Modern versions of JavaScript allow an optional strict mode, which will
help to reduce errors in your code. The most significant change is that it
will disallow creating new variables without declaring first.
Why would that be useful? JavaScript’s relaxed attitude to declaring
variables can get you into trouble. For example:
» value = 5;
» Value = value + 2;
» console.log(value);
19
Chapter 1 Working with JavaScript
To enable strict mode, the script must include the following string as
the first statement:
'use strict';
This statement can appear after blank or comment lines but will not
work if it is placed after another JavaScript statement.
The JavaScript console will probably ignore the use strict
statement, so we won’t bother with it here. Later, when writing
real code, we’ll definitely be using it.
Comments
JavaScript allows you to insert comments into the text. Comments are text
that will not be interpreted by the JavaScript interpreter.
Comments are used for the following reasons:
20
Chapter 1 Working with JavaScript
For example:
Comments can be used for any purpose at all. Previously you see three
very common uses:
Nested Comments
You cannot nest blocked comments; that is, you cannot place one block
comment inside another. This is because the second block start will be
ignored and the first block end will terminate the comment.
21
Chapter 1 Working with JavaScript
For example:
Data Types
Partly for historical reasons, partly for efficiency reasons, and partly for
other reasons, programming languages generally distinguish between
types of data. In JavaScript, there are three basic types:
• Numbers.
22
Chapter 1 Working with JavaScript
Numbers
Numbers are one of the fundamental data types in JavaScript. Many
languages distinguish between different types of numbers, such as integers
and floating-point (decimals), but JavaScript dispenses with the difference
and treats all numbers the same.
This does lead to a notorious quirk:
» console.log(0.1 + 0.2);
0.30000000000000004
Writing Numbers
Numbers can be written with or without decimals:
» 3
» 3.0
» 25.4
23
Chapter 1 Working with JavaScript
Arithmetic
JavaScript will perform arithmetic using the standard rules. Generally this
uses values and operations.
The main arithmetic operators are the following:
Symbol Operation
+ Addition
- Subtraction
* Multiplication (×)
/ Division (÷)
% Remainder
24
Chapter 1 Working with JavaScript
Associativity
Consider the following expression:
» 12 - 3 + 4
If you were tempted to perform the second operation first, giving you
12-7 or 5, you would be wrong. JavaScript will evaluate this expression
from left to right, giving you 9+4 or 13.
However, the rule of associativity only applies when the operators have
the same precedence.
Precedence
Consider the following expression:
» 1 + 2 * 3
1. (...)
2. * or /
3. + or –
25
Chapter 1 Working with JavaScript
» 12 / 2 * 3
Remainder (%)
Sometimes, when you divide two integers (whole numbers), you don’t
want the whole result. Sometimes, what you want is the remainder. The
operator is written as a percent sign (%). For example:
» 100 % 7;
26
Chapter 1 Working with JavaScript
Strings
Strings are containers of text and other characters. That is, they are strings
of characters. JavaScript has a few simple string operations available
for working with strings as well as a rich collection of functions that can
manipulate strings. There are also functions that deal with converting
between strings and numbers.
Strings may or may not mean something to the reader:
'qwerty'
'Hello'
'23'
Note the last expression. It looks like number, but it isn’t: it’s stored
differently and will be treated differently when the time comes. However,
JavaScript may well convert it to a number if the situation calls for it.
String Literals
A string literal is part of the code that defines a string. The distinction may
be subtle, but it is important.
27
Chapter 1 Working with JavaScript
» message = 'Hello';
The string literal 'Hello' defines the string Hello that is stored in
the variable message. The string itself doesn’t include the quotes that
were used to define the string. The quotes, however, are written so that
JavaScript won’t confuse the string literal with some other code.
JavaScript allows you to use single or double quotes for string literals,
as long as you finish the way you started:
'Hello'
"Hello"
» console.log('<img src="thing.jpg">');
Here, the single quote is used to allow double quotes in the string.
Some users use double quotes to allow single quotes to be used as
apostrophes:
» console.log("Don't Worry");
» console.log("Don't Worry");
» console.log('Don't Worry');
28
Chapter 1 Working with JavaScript
One thing to be careful about is that JavaScript does not use either
single or double typographic quotes to define a string:
» 'hello'
← "hello"
Empty Strings
An empty string is a zero-length string with no characters:
» var a = '';
» var a = "";
Unicode
JavaScript strings are encoded internally in Unicode, specifically UTF-16.
From a practical point of view, that means that a JavaScript string can
include any character from any language in the world. It also means that a
string usually uses 2 bytes per character, though some characters will take
more. None of that affects our normal use of strings.
29
Chapter 1 Working with JavaScript
» var a = 'Wilma';
» a = 'Betty';
This requires creating a new string for the variable a and disposing of
the old one. This happens automatically, so we don’t normally need to
bother about it. However, it does mean that string operations do a lot of
work in the background and so are regarded as expensive.
Escaped Characters
Sometimes, a string literal is supposed to include awkward characters,
such as the quote characters or special characters. To allow special
characters, the backslash character (\) is used as the so-called escape
character. For example:
Here, the escape sequence \' is used to embed the single quote
that would otherwise prematurely terminate the string. (Of course,
using a typographical apostrophe would also have avoided the problem
altogether.)
Note that the backslash is not actually part of the string. It is simply
used to modify the next character. If you really wanted a backslash, you
would need to escape it as well:
30
Chapter 1 Working with JavaScript
The escape character can also be used to insert a few special characters
such as the tab (\t) or new line (\n):
» console.log('Fruit:\tApple\nColour:\tRed');
← Fruit: Apple
Colour: Red
Template Literals
JavaScript strings can contain any character at all, but string literals have
limitations. In particular, you can’t include line breaks. For example, this
would generate an error:
Normally, you would work around this either by including the escaped
line break (\n):
Note that template literals quote strings using the grave character, the
so-called backtick (`).
31
Chapter 1 Working with JavaScript
» var price = 3;
» var quantity = 4;
» console.log(`Total: ${price*quantity}`);
String Concatenation
In JavaScript, there is only one direct operation with strings:
concatenation. This will join them together.
JavaScript uses the addition sign (+) for concatenation, which is a
potential source of error, as we’ll see later.
Examples of concatenation are
» var a = 'Hello';
» console.log(a + 'Fred');
Note that concatenation does not add any extra spaces. If you want
them, you will have to add them yourself:
The fact that concatenation uses the same symbol as addition can
cause some odd results when combining the two operations. For example:
This is because JavaScript reads the expression from left to right and
interprets the plus sign as it seems appropriate.
32
Chapter 1 Working with JavaScript
If you really wanted to add the numbers first, you would need to group
them with parentheses:
» console.log('abc' + (1 + 2));
Later, we will need some numbers from a form or a dialog box. Since
all data will be treated as text, we can run into this problem if we are trying
to add to them.
» var a = 'abcdefghijklmnop';
» console.log(a.length);
» console.log('abcdefghijklmnop'.length);
» var a = 'Hello';
» console.log(a.toUpperCase());
← HELLO
» console.log(a.toLowerCase());
← hello
These methods don’t change the original string; they return a new string.
33
Chapter 1 Working with JavaScript
Boolean
Boolean values are either true or false. (Boolean is named after the
mathematician George Boole who pioneered mathematical logic.)
You can assign boolean values directly:
» var ok = false;
» var a = 3, b = 4;
» var ok = a > b;
» console.log(ok);
» var a = 3, b = 4;
» var ok = a > b;
» if (ok) console.log('bigger');
Boolean values are often used when you need to test a value to decide
what to do next. Two such cases are the if statement and the while
statement. You see a simple example of an if statement shown previously.
The while statement is an example of iteration: repeating a process a
number of times. A simple example of the while statement is
34
Chapter 1 Working with JavaScript
Using Boolean
Often, we use a boolean value without realizing it. For example:
» var a = 3, b = 5;
» if(a>b) console.log('a is bigger');
a = 3; b = 5;
ok = a>b;
if(ok) alert ('a is bigger');
Why would you do this? It would be useful if you need to use the same
test again later:
a = 3; b = 5;
ok = a>b;
if(ok) alert ('a is bigger');
...
if(ok) ...;
» var a = '3';
» console.log(2 * a);
← 6
35
Chapter 1 Working with JavaScript
Since you can only multiply numbers, JavaScript will convert the string
'3' to a number.
This isn’t always possible. If the string can’t be interpreted as a
number, then it can’t be converted. For example:
» var a = 'hello';
» console.log(2 * a);
NaN
Note that the result is not technically an error. It’s a special value. We’ll
look again at NaN shortly.
Sometimes, JavaScript doesn’t feel the need to convert anything, even
if it can. For example:
» var a = '3';
» console.log(2 + a);
← 23
will give you a different result. In a sense, JavaScript can “add” strings,
which really concatenates them. So it is the 2 which is converted to a string
for concatenation.
If you want to force the issue, you can manually convert a string to
a number:
» var a = '3';
» console.log(2 + parseInt(a));
36
Chapter 1 Working with JavaScript
» var a = parseInt('123abc456');
» console.log(a);
← 123
will interpret the 10110110 as a binary (base 2) number and return 182.
However, even if you want the base 10, it is most reliable to specify the
base as such:
The parseFloat() function will also change the string to a number but
also allows decimals. However, you cannot use this with other bases.
37
Chapter 1 Working with JavaScript
isNaN( )
If you need to test whether a conversion to a number is possible, you can
use the isNaN() function (is Not a Number), and yes, there are two capital
Ns in the name. This function actually tests for failure of the conversion.
For example:
» var b = '456a';
» var a = b*1;
» if (isNaN(a)) console.log('oops');
The == operator tests whether two values are the same. This feature is
nifty, but you might end up confusing other coders.
38
Chapter 1 Working with JavaScript
» var a;
» a = 3;
» console.log(typeof(a));
"number"
The typeof() function gives the data type of the current value.
If you then change the value to a different type, the typeof() function
will give the new type:
» a = 'hello';
» console.log(typeof(a));
"string"
In the trade, we say that JavaScript is dynamically typed. The data type
will depend on the current value.
Functions
Functions are another mainstay of any programming language, and we will
be using and writing functions throughout the course.
We’ll be looking at functions in more depth later. However, for now, we
will develop an idea of what they are and how we use them.
39
Chapter 1 Working with JavaScript
» function twice(number) {
var value = number * 2;
return value
}
← undefined
If you find that the console doesn’t let you type in the rest of the code,
you may need to press to get the extra lines.
Just like the var statement (and let and const), the output of a
function declaration is undefined. It doesn’t mean that the function is
undefined – just that there’s no output value from the definition.
This sample function is, of course, trivial, but you can test it this way:
» var a = 3;
» var b = twice(a);
» console.log(b);
6
40
Chapter 1 Working with JavaScript
» function thing() {
// ...
}
The code is also always wrapped in braces, even if it’s only one line
of code.
Void Functions
To begin with, JavaScript doesn’t have void functions, so this might be a
little misleading. Some other languages do have void functions, sometimes
referred to as procedures.
If JavaScript did have void functions, they would be functions that
don’t return a result. They can still do something useful, such as display a
message, but they don’t calculate any result to output.
In JavaScript, all functions return a result. However, unless you specify
otherwise, the result is a special magic value, undefined.
Whenever you see return without a value, it always means return
undefined;. Whenever you have a function without a return, there’s an
implied return undefined at the end.
One function you’ve already seen that does this is the console.log()
function. Whenever you call it, the console shows you the output, which is
undefined.
41
Chapter 1 Working with JavaScript
» alert('This is a message');
undefined
42
Chapter 1 Working with JavaScript
43
Chapter 1 Working with JavaScript
If you click OK, then the result will be whatever text you entered in the
text box. If there were no text, the result would be an empty string. If, on
the other hand, you click cancel, the result would be null, a special value
that means “nothing.”
As you see, these functions hold everything up till the user has
responded. That’s OK for testing and troubleshooting, but it’s very
disruptive for a real situation, so you should limit their use. You’ll also
find that, by design, you won’t be able to change their appearance or the
button text.
Array items are numbered from zero. In the preceding example, the
items are numbered from 0 to 3. You can get the number of items in the
array with the .length property:
» console.log(fruit.length);
4
44
Chapter 1 Working with JavaScript
To get one of the array items, you use square brackets for the
item number:
» console.log(fruit[2]);
cherry
» fruit[2] = 'cantaloupe';
You can also add new items or remove them. Later, we’ll work more
with arrays.
As we said, arrays are specialized objects. In general, objects are
collections of data and functions. However, in JavaScript, you’ll find that
functions are handled as data, so you can simply say that JavaScript objects
are collections of data.
This data can be
• Functions
• Other objects
In JavaScript, some very important objects will relate to the contents of
the web page.
We will look more at objects later.
45
Chapter 1 Working with JavaScript
For example:
var x = 3;
console.log(x);
var x;
console.log(x);
console.log(y);
46
Chapter 1 Working with JavaScript
» var a = undefined;
Of course, you may end up with an error if you plan to use the variable
later when another real value is expected.
JavaScript Scripts
Every web page starts with the HTML. In turn, the HTML may contain
references to images and other media content, as well as CSS and
JavaScript.
47
Chapter 1 Working with JavaScript
You’ll see later that CSS will also play an important part of our
JavaScript projects, but for now, we’ll concentrate on how JavaScript is
added to a web page.
<html>
<head>
...
<script type="text/javascript">
// JavaScript goes here
</script>
<head>
<body>
...
</body>
</html>
48
Chapter 1 Working with JavaScript
49
Chapter 1 Working with JavaScript
There are a few other attributes, but we won’t need them for our
projects.
In general, our script tag should look like this:
50
Chapter 1 Working with JavaScript
Deferring Loading
In this book, we will be putting the <script> element fairly early, inside
the <head> element, which is standard practice. You can also put the
element much further down, and you may even see it placed at the very
end of the file.
The browser will normally begin loading and processing the JavaScript
as soon as script element is encountered. While it is working on the
JavaScript, it will pause loading and processing the rest of the HTML. There
are two problems with this:
This will allow the browser to continue processing the rest of the
HTML, and the browser won’t actually run the JavaScript until after the
HTML is finished.
51
Chapter 1 Working with JavaScript
You can’t use defer with an inline script (one without the src
attribute). If you have inline JavaScript, you will need an older technique.
Error Handling
It’s likely that you will make some errors in your JavaScript code, and some
of them can be very hard to locate.
Historically, JavaScript was pretty quiet about some errors, reasoning
that the visiting user can’t do much about the developer’s errors anyway.
However, as a developer, this coyness can be infuriating, so you’ll want the
errors to be more obvious.
The following code will make some of them easier to identify:
52
Chapter 1 Working with JavaScript
Strict Mode
For your convenience, JavaScript was pretty relaxed about some rules. So
relaxed, in fact, that it let you make a lot of errors without treating them
as such. Modern JavaScript allows you to be a little more strict about
some rules.
To put JavaScript in strict mode, begin your script with
'use strict';
This may seem a rather odd way of changing a setting, but it’s done
this way simply to make it compatible with older browsers. Older
browsers can’t be expected to understand new commands, but they’ll
happily ignore a stand-alone string because that’s what they’ve
always done.
If you incorporate someone else’s code into your own, you may run
afoul of strict mode if they have been a little careless, and you’re probably
justified in feeling a little aggrieved. However, when writing your own code,
you should always use strict mode.
53
Chapter 1 Working with JavaScript
Some Templates
Putting all of this together, here are some recommended templates for your
JavaScript:
HTML Template
In HTML, you will need to include or reference the JavaScript. If the script
is to be included inline, you can add
<head>
<script type="text/javascript">
</script>
<!-- etc -->
</head>
If the script is external, which will be the normal case, you can
reference it as
<head>
<script type="text/javascript" src="..." defer
crossorigin="anonymous">
</script>
<!-- etc -->
</head>
54
Chapter 1 Working with JavaScript
In some cases, you will load more than one JavaScript file, so there will
be multiple links.
If you’re not using some sort of web server for the following
exercises, the crossorigin attribute may cause problems. Instead,
you should use the following template:
<script type="text/javascript" src="..." defer></script>
J avaScript Template
In the JavaScript file, you can start with
/* something.js
================================================
Description ...
================================================ */
'use strict';
window.onerror=function(message,url,line) {
alert(`Error: ${message}\n${url}: ${line}`);
};
init();
function init() {
}
55
Chapter 1 Working with JavaScript
Summary
• Statements
• Expressions
56
Chapter 1 Working with JavaScript
57
Chapter 1 Working with JavaScript
Coming Up
Now that you have an idea of how JavaScript works, we’ll put all of that into
practice.
In the following chapters, we’ll learn more about the language and how
to use it. We’ll also learn more about using JavaScript to interact with the
web page.
First, however, we’ll look at some of the JavaScript concepts more
seriously. In the next chapter, we’ll implement a simple guessing game.
In the process, we’ll learn more about the main JavaScript structures
of conditions, repetitions, and functions. We’ll also start working with
accessing the elements on the web page.
58
CHAPTER 2
Project: A Simple
Guessing Game
JavaScript can be used to write some pretty sophisticated games, but that’s
not going to happen here. Instead, we’ll write a very simple guessing game.
This will help us to develop skills in some of the important concepts in the
JavaScript language.
In this chapter, we’ll learn about
• Functions
• Repetition with do ... while blocks
The point of this exercise is not the game. Rather we’ll see some of the
important JavaScript principles and structures in action.
Some of the code samples that follow are meant to be tried out in the
console. If so, they will be prefixed with the console prompt ».
Preliminary Code
Every web page starts with the HTML. In turn, the HTML will load the
JavaScript, and, in most cases, include various HTML elements that will
interact with the JavaScript.
In this case, the main element to interact with the JavaScript is a simple
button that will be used to start the game:
<!DOCTYPE html>
<!-- game.html -->
<head>
...
<script type="text/javascript" src="scripts/game.js"
crossorigin="anonymous" defer></script>
</head>
60
Chapter 2 Project: A Simple Guessing Game
For the JavaScript file, we can begin with the standard template:
/* game.js
================================================
Guessing Game
================================================ */
'use strict';
window.onerror=function(message,url,line) {
alert(`Error: ${message}\n${url}: ${line}`);
};
init();
function init() {
}
Later in this chapter, we’ll move some of the code to a different file. By
then, we’ll have written a function to generate random numbers, and it’s
the sort of thing you might want to use in another project.
There will be an empty file in the scripts folder called library.js. We
won’t need it yet, but it doesn’t hurt to have it open.
For the game, we will create another function called playGame(). The
idea is to trigger this function by clicking on the button.
62
Chapter 2 Project: A Simple Guessing Game
The first step will be to identify the button and link it to the function:
...
init();
function init() {
let button = document.querySelector('button#startgame');
button.onclick=playGame;
}
function playGame() {
}
63
Chapter 2 Project: A Simple Guessing Game
For the onclick event handler, you define the function using
parentheses. You then assign a reference to the function to be called later;
that is, you assign playGame without parentheses. The function will be
called later in response to the click event.
• Repeat.
• … until correct.
64
Chapter 2 Project: A Simple Guessing Game
function playGame() {
// Set up the data
// Initialise
// Repeat
// User guesses
Using comments is a good way of outlining your plan. When you write
the code, they’ll also serve as headings.
When you do write your code, you may find that the plan needs
adapting, and some of the comments will change or disappear. That’s part
of the development process.
function playGame() {
// Set up the data
let answer, guess;
...
}
65
Chapter 2 Project: A Simple Guessing Game
We’ll use the newer let statement instead of the more traditional var
statement. At this stage, it won’t make any difference, but it’s a good habit
to get into, since let is more predictable. Anything done in the console,
however, will still use the var statement for flexibility.
The variable names may not sound very imaginative, but it’s hard to
miss their meaning:
function playGame() {
// Set up the data
let answer, guess;
// Initialise
answer = 57;
Now 57 may not be very random, but at this stage, we need to use
a value we already know; you can use any number you like, of course.
When developing, you should work with predictable data so that you can
see whether the rest of the code is doing its job properly. Once you’re
convinced that it is, you can work with unknown values. Later, we will
introduce a more random number.
Repeating Code
The next part will be repetitive, at least for JavaScript. We want to repeat
asking the user for their guess and reporting on the result until the number
has been guessed.
66
Chapter 2 Project: A Simple Guessing Game
do {
} while( ... );
function playGame() {
...
do {
67
Chapter 2 Project: A Simple Guessing Game
do {
guess = prompt('Guess the number:');
} while(guess != answer);
The message here is a little abrupt, but you can supply any message
you like.
Note that you have no control over the appearance of the prompt, nor
the contents of the buttons. This is a small security measure that makes it
harder for a website to fake a system message. It also means that you will
have to word your message so that “OK” and “cancel” make some sort
of sense.
68
Chapter 2 Project: A Simple Guessing Game
do {
guess = prompt('Guess the number:');
} while(guess != answer);
69
Chapter 2 Project: A Simple Guessing Game
You don’t always need braces for the conditional code if there’s only
one line. For example:
» var a = 4, b = 3;
» if(a > b) {
alert('Bigger');
}
» if(a > b) alert('Bigger'); // Same Thing
do {
guess = prompt('Guess the number:');
} while(guess != answer);
70
Chapter 2 Project: A Simple Guessing Game
In this case, there’s only one other possibility, so we can use the else
clause statement, which catches the rest:
do {
guess = prompt('Guess the number:');
} while(guess != answer);
This is a fairly full example of the if statement. In reality, not all will
have else if clauses, and not all will have an else clause. If they do,
however, they must be in the right order.
do {
guess = prompt('Guess the number:');
71
Chapter 2 Project: A Simple Guessing Game
} while(guess != answer);
For now, each conditional block has only one statement (the alert()
function), and we would normally remove the braces. However, we’ll keep
them there for a future improvement.
72
Chapter 2 Project: A Simple Guessing Game
• Enter a zero.
You will notice a few things, some of which will start to become
annoying:
When we start randomizing the number, you will see a few other
problems.
function playGame() {
// Set up the data
let answer, guess;
let message;
73
Chapter 2 Project: A Simple Guessing Game
// Initialise
answer = 57;
message = 'Guess the Number';
...
}
guess = prompt(message);
That should give exactly the same behavior so you might wonder why
we bothered. Even without doing anything else, we have already improved
the maintainability of the code by moving the text of the message into the
initialization section. This way, all of your arbitrary, flexible values are in
one place rather than sprinkled throughout the code.
But the real benefit will be in how we change the message in response
to the guess:
do {
guess = prompt(message);
} while(guess != answer);
74
Chapter 2 Project: A Simple Guessing Game
Here, we have replaced the message with Too Low or Too High. It won’t
show immediately, but since the while statement will go on to repeat, the
new message will be displayed in the prompt the next time round.
If, on the other hand, the guess is correct, there won’t be a next time
round, so the Correct message is displayed immediately using the alert()
function as before.
When you test the code now, it won’t be quite so tedious.
75
Chapter 2 Project: A Simple Guessing Game
/* game.js
================================================
Guessing Game
================================================ */
'use strict';
function random(max) {
// returns a random number from 1 ... max
return Math.floor(Math.random()*max)+1;
}
...
You’ll notice that we’ve nearly given the function the same name as the
original. The difference is that the original is part of the Math object while
this isn’t. Naming functions this way is OK, as long as they are in different
contexts but philosophically do the same sort of thing.
76
Chapter 2 Project: A Simple Guessing Game
JavaScript will allow you to create a function with exactly the same
name as another, in which case the new one will replace the old.
That’s not always a good idea but is sometimes done accidentally.
77
Chapter 2 Project: A Simple Guessing Game
function playGame() {
// Set up the data
let answer, guess;
let message;
// Initialise
// answer = 57;
answer = random(100);
message = 'Guess the Number'
...
}
If you want to test the game now, you’ll have to guess intelligently;
otherwise, you might be guessing for a while. The simplest way is to
guess by halves: start at 50, then half way either up or down, depending
on the result. This is called a binary search, because you’re dividing the
possibilities by two.
Technically, the number isn’t truly random, but for our purposes, it’s
unpredictable, and that’s close enough.
One feature of random, or nearly random, numbers is that they might
be anything. If the number happens to be 1 or 100, well, that can
happen, and it’s not (yet) a reason to suspect that something’s not
working properly. Try it again a few times before you start to panic.
78
Chapter 2 Project: A Simple Guessing Game
do {
guess = prompt(message);
if(guess === null) return;
...
} while(guess != answer);
Yes, that’s three = characters. You’ll see why in the next section.
Remember that the whole game is played inside a function. The
return statement does two jobs:
79
Chapter 2 Project: A Simple Guessing Game
» var a, b;
» a = 3; b = 4; // assignment
» if(a == b) console.log('yes'); // comparison
else console.log('no');
← no
The comparison operator uses two = characters: ==. Here, we’ll get no,
because the values aren’t the same.
Now, here’s trap number 1. If you use the assignment operator
instead of the comparison operator, it will do something, but you won’t be
comparing the values. Instead, you’ll be reassigning a variable:
» if(a = b) console.log('yes'); // assign b to a
else console.log('no');
← yes
In this case, a becomes 4, and you’ll get a yes. But not for the reason
you might think. Let’s try it again, with different values:
» var a, b;
» a = 3; b = 0;
» if(a = b) console.log('yes');
else console.log('no');
← no
80
Chapter 2 Project: A Simple Guessing Game
This time, you will get a no. As you see, the only thing that’s different is
that b is now 0.
When experimenting with the console, you will have noticed that
a statement such as a = 3 has a result, which is the value you have just
assigned.
In the expression if(a = b) ..., two things are happening. First, the
variable a gets a new value. Second, the new value is tested in the if(): if
the value is truthy, such as a nonzero number, the test passes; if the value
is falsy,1 such as zero, the test fails.
There are many developers around who offer all types of advice for
avoiding this trap, but it’s all bad advice. Just get used to it. You will make
this mistake, and you’ll eventually get better at identifying it.
As for trap number 2, it is not so bad but can still catch you out. Let’s
change one of the values to a string and try a proper comparison:
» var a, b;
» a = 3; b = '3';
» if(a == b) console.log('yes');
else console.log('no');
← yes
Is the number 3 the same value as the string '3'? The answer depends
on how relaxed you want to be about equality. In JavaScript, one can
readily be interpreted as the other, so it’s close enough.
If that’s not good enough, you can be a little stricter:
1
I did not make up these terms, and they’re not real words in English, but they’re
commonly used. Sorry.
81
Chapter 2 Project: A Simple Guessing Game
The third operator is the identity operator and is written with three
characters: ===. Basically, it tests whether the values are the same without
reinterpreting.
You won’t see the identity operator much, but it can be particularly
important when comparing the falsy values. There are many values that
will be interpreted as false, and occasionally you may need to distinguish
between them. That’s where the identity operator comes in.
guess = prompt(message);
if(guess === null) return 'game cancelled';
function playGame() {
...
82
Chapter 2 Project: A Simple Guessing Game
function playGame() {
// Set up the data
let min, max;
let answer, guess;
let message;
// Initialise
[min,max] = [1,100]; // alternatively: min=1;
max=100;
answer = random(max);
message = 'Guess the Number'
...
}
83
Chapter 2 Project: A Simple Guessing Game
The two new variables, min and max, are first declared as usual and
then initialized to 1 and 100, respectively. There are a few notable features
of how this is written:
84
Chapter 2 Project: A Simple Guessing Game
The next thing is to use the new variables in the prompt message:
do {
guess = prompt(`${message}\nFrom ${min} to ${max}`);
if(guess === null) return 'game cancelled';
...
} while(guess != answer);
The message has been embedded in the newer type of string literal, a
template literal. The most immediate feature is the use of the so-called
backtick (` ... `) instead of the usual single or double quotes. This tells
JavaScript it can look inside for expressions.
Inside the string is a mix of expressions and ordinary text. Expressions
are wrapped inside the special code ${ ... }. In this case, the expressions
are just the variables. This is the easiest way of mixing a string with values.
You will also see the \n inside the string. This will work in ordinary
strings as well and is a code meaning New Line.
To begin with, you will see something like Figure 2-4.
As the game progresses, the limits will change. This can be done while
testing the guess:
85
Chapter 2 Project: A Simple Guessing Game
That’s why we have got the braces for each part of the if statement: so
that we can include additional statements.
If the guess is too low, you’ll need to guess higher next time; the new
minimum should be one more than the previous guess. If the guess is too
high, your next guess should be lower, and the new maximum should be
one less than the previous guess.
When you test this now, it will work a little, but you will see another
problem. You may get a message something like From 501 to 100.
Here, we run across another problem with JavaScript. Recall the
JavaScript uses the + operator for both concatenation of strings and
addition of numbers, with a preference for concatenation. As it turns out,
all inputs from the prompt are strings.
Till now, that didn’t matter, since comparing numbers to strings, as we
do with the < and >, will cause the guess to be temporarily converted to
numbers for the task. It’s also fine with the expression guess - 1; you can’t
subtract strings, so JavaScript has again temporarily converted guess for
the task.
However, when it comes to guess + 1 operation, JavaScript will treat
them both as strings and concatenate them; this is where something
like 501 comes from. The solution will be to force the guess value into
a number. The simplest way to do that is with the parseInt() function,
which will transform the guess variable to an integer:
86
Chapter 2 Project: A Simple Guessing Game
do {
guess = prompt(`${message}\nFrom ${min} to ${max}`);
if(guess === null) return 'game cancelled';
guess = parseInt(guess); // convert to a number
...
} while(guess != answer);
do {
guess = prompt(`${message}\nFrom ${min} to ${max}`);
if(guess === null) return 'game cancelled';
guess = parseInt(guess) || 0; // convert to a number
...
} while(guess != answer);
87
Chapter 2 Project: A Simple Guessing Game
/* random(max)
================================================
Returns a random integer between 1 and max
================================================ */
88
Chapter 2 Project: A Simple Guessing Game
function playGame() {
// Set up the data
let min, max;
let answer, guess;
let message;
// Initialise
[min,max] = [1,100];
answer = random(100);
message = 'Guess the Number';
do {
guess = prompt(`${message}\nFrom ${min} to ${max}`);
if(guess === null) return 'game cancelled';
guess = parseInt(guess) || 0;
89
Chapter 2 Project: A Simple Guessing Game
Summary
In this chapter, we’ve explored some important programming concepts
using a simple guessing game project.
Comments
Comments can be used to explain parts of the code, as well as to outline
sections of the code. They can also be used to disable parts of the code for
testing.
Organization
It’s best, of course, if you can plan all of your code in advance. However,
while building the project, we followed a few organizational principles:
Repetition
The while statement is used to repeat a block while a condition is true.
90
Chapter 2 Project: A Simple Guessing Game
Conditional Blocks
The if statement is used to run code conditionally – when a condition
tests as true.
The if can be followed by one or more else if blocks and can be
followed by an else block.
Multi-line blocks are enclosed in braces. For single statements, braces
aren’t required, but they can be used if you think it makes it more readable,
or if you think you may need more statements later.
When testing equality, you need to use the equality operator (a == b).
It’s a common mistake to use the assignment operator instead (a = b),
which doesn’t test equality but changes the first variable.
Dialog Boxes
The prompt() function can be used to get user input, while the alert()
function can be used to send a message.
Cancelling the prompt() dialog box doesn’t cancel everything. If you
want it to do more, such as exiting the game, you’ll have to write the code.
Values coming from a prompt() are strings and never numbers.
Although JavaScript will often reinterpret the string as a number if the
situation calls for it, it won’t if you use the addition operator (+); instead, it
will concatenate. Sometimes, you need to convert the string yourself using
something like parseInt().
Coming Up
This game was pretty dull as far as a web project goes. The only
involvement of the web page was to start the game via a button on
the page.
91
Chapter 2 Project: A Simple Guessing Game
In the next chapter, we’re going to have a good look at interacting with
the web page. We’ll learn how to locate elements on the page and how to
change their content or other attributes. We’ll even create a few elements
to add to the page.
In Chapter 4, we’ll put all of that into practice.
92
CHAPTER 3
Manipulating HTML
Elements
All web pages are written initially in HTML, but this code is only text to
begin with. The browser then takes this HTML code, together with any
associated CSS, and uses it to generate the visible content of the page.
As the browser loads and implements the HTML, it also creates an
internal representation of the document for JavaScript. This is called the
Document Object Model, or DOM to its friends. We can use the DOM to
manipulate various parts of the web page in JavaScript.
In this chapter, we’re going to look at
To really get your hands on all of this, there is a sample page for you to
experiment on:
• Also load the page in your coding editor, so you can see
the underlying HTML.
94
Chapter 3 Manipulating HTML Elements
95
Chapter 3 Manipulating HTML Elements
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta>
<link>
<link>
</head>
<body>
<header>
<div></div>
</header>
<main>
<h1></h1>
<figure>
<img>
<figcaption></figcaption>
</figure>
<div>
<h2></h2>
<p></p>
96
Chapter 3 Manipulating HTML Elements
<h2></h2>
<div>
<p></p>
<p></p>
<p></p>
</div>
</div>
</main>
<footer>
<p></p>
<p></p>
</footer>
</body>
</html>
97
Chapter 3 Manipulating HTML Elements
98
Chapter 3 Manipulating HTML Elements
Some of the terms come from the tree metaphor, while some come
from a family metaphor.
Sometimes, you will need to work with specific nodes, while
sometimes, you will work with nodes that are related to specific nodes.
Finding Elements
One thing you will definitely need to do in JavaScript is find the elements
you want to work with. In the past, you might have used any of the
following functions:
• document.getElementsByTagName(): Returns a
collection of elements using a specific tag, such as
h1 or li.
• document.getElementsByClassName(): Returns a
collection of elements with a specific class attribute.
99
Chapter 3 Manipulating HTML Elements
These are roughly in historical order. In reality, the last one, document.
getElementsByClassName(), didn’t get much love since it was late coming
to Internet Explorer, which was dominant at the time, by which time
something better became available.
Note that all but one of the aforementioned functions have the plural
Elements in their name, to reflect the fact that they return a collection.
Only document.getElementById() has a singular Element in its name to
reflect the fact that there should be only one match.
You no longer need any of these functions, but you will still see them in
older code or code written by older developers. That’s especially the case with
document.getElementById(). From here on, however, we won’t be using them.
There are now two functions that will meet all of your needs:
• document.querySelector(): The first element
matching the selector.
• If there is no match, you will get null.
Method Meaning
100
Chapter 3 Manipulating HTML Elements
Method Meaning
Here, the whole document is searched for the first element that
matches div. The resulting div element is further searched for all of the p
(paragraph) elements.
You’ll see more on using the tag names and other selectors in the next
section.
CSS Selectors
JavaScript isn’t CSS, but it uses CSS selectors in the .querySelector() and
.querySelectorAll() methods. A CSS selector is the expression that CSS
uses to identify which elements are affected by the CSS rules.
Over the years CSS has become very sophisticated, and there are many
subtle ways of targeting elements. For our part, we are mostly interested in
the following:
101
Chapter 3 Manipulating HTML Elements
Tag Selectors
The basic selector is the tag selector. This is a simple HTML tag and refers
to all elements with that tag. For example:
102
Chapter 3 Manipulating HTML Elements
Apart from elements that are meant to be unique, you might need
further help in limiting the results by using an id.
id Selectors
In HTML, there is often an id attached to one of the elements
to be targeted. For example, one paragraph on the page has the
following HTML:
p#something {
border: thin solid #666;
padding: 0.25em 0.5em;
}
In JavaScript, you can reference the element using the CSS selector:
» var p = document.querySelector('p#something');
» var p = document.querySelector('#something'); // same
» var p = document.getElementById('something'); // older
103
Chapter 3 Manipulating HTML Elements
Class Selectors
If there’s more than one element you want to match, then the id is
unsuitable. Instead, you’ll see the class attribute that describes a group of
multiple elements. For example, in the sample document, there are several
paragraphs, some of which have a class attribute:
p.special {
font-style: italic;
}
As with the id, you can use querySelector() to select the first match,
but unlike the id, it also makes sense to select multiple matches with
querySelectorAll():
» // First match
» var p = document.querySelector('p.special');
» // All matches
» var pp = document.querySelectorAll('p.special');
» // less specific
104
Chapter 3 Manipulating HTML Elements
<div id="separate">
<p> ... </p>
<p> ... </p>
<p> ... </p>
</div>
div#separate>p {
border: thin solid white;
}
div#separate p {
padding: 1em;
}
105
Chapter 3 Manipulating HTML Elements
» var pp = document.querySelectorAll('div#separate>p');
console.log(pp); // children
» var pp = document.querySelectorAll('div#separate p');
console.log(pp); // descendants
In this case, they are the same thing, since there are no other
containers in the example. If, however, you start with div#content, you’ll
see a difference:
» var pp = document.querySelectorAll('div#content>p');
console.log(pp); // children
» var pp = document.querySelectorAll('div#content p');
console.log(pp); // descendants
<ul id="children">
<li>Animals</li>
<li>Instruments</li>
<li>Fruit
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</ul>
</li>
</ul>
106
Chapter 3 Manipulating HTML Elements
• Animals
• Instruments
• Fruit
• Apple
• Banana
• Cherry
You can select the various parts of the list using the following:
107
Chapter 3 Manipulating HTML Elements
The textContent property is what it sounds like. It’s the content of the
element as text. You’ll see more on textContent later.
The for structure sets up a counter (let i=0) and counts through the
collection of children, referencing each member with the index i. The
.toUpperCase() method returns an upper case version of the original text.
Note that replacing the textContent will have the side effect of
replacing the nested list with only the text: it’s no longer another list.
108
Chapter 3 Manipulating HTML Elements
Related Elements
When working with the DOM, you are sometimes interested in elements
nearby. You saw previously how different nodes are related. Here is a list of
some of the ways you can access them:
109
Chapter 3 Manipulating HTML Elements
The names are clear enough as to what these methods do. The first
methods (childNodes and children) return a collection, while the others
return a single result.
The first three in the previous table work from the parent node. In this
case, it’s the ul unordered list, and you would be referring to the li items
inside. The last two work from the child nodes. In this case, they’re the li
list items, and you would be referring to adjacent sibling list items or to the
parent ul list.
You’ll notice that there are two versions, due to a quirk in the way
HTML works. Technically, all the white space between elements in the
HTML, and even comments, is part of the DOM. For example, suppose you
have a list and items:
<ul id="children">
<li>Child</li>
<li>Child</li>
<!-- Nested List -->
</ul>
The indentation, line breaks, and comments are all considered DOM
nodes. Most of the time, you don’t care and are only interested in the list
and items, without bothering with the extra spacing or comments. The
extra spacing and comments certainly won’t appear in the rendered page,
so they’re only visible if you view the source.
110
Chapter 3 Manipulating HTML Elements
» var ul = document.querySelector('ul#children');
» console.log(ul.children); // Only List Items
» console.log(ul.childNodes); // May contain other code
The second column properties are newer, but all current browsers
support them. You probably never want the older first column properties.
» var p = document.createElement('p');
The argument is a string that is a simple HTML tag, without the angle
brackets. You can even make one up:
» var p = document.createElement('thing');
111
Chapter 3 Manipulating HTML Elements
» var p = document.createElement('p');
» p.textContent = 'This space for rent ...';
» var h1 = document.querySelector('h1');
» h1.textContent = 'Hello<br>Goodbye';
» var h1 = document.querySelector('h1');
» h1.innerHTML = 'Hello<br>Goodbye';
the <br> inside the string will be interpreted as an HTML line break, and
you will see two lines of text.
In general, it is safer to use textContent unless you really need the
additional HTML.
There is also a property called innerText, which has had a
mixed history. Originally it was an unofficial property sup-
ported only by Internet Explorer, while the other browsers
only supported the official textContent property. Today, both
are supported by all modern browsers. However, there is a
small difference between the two, which makes textContent
the preferred property.
112
Chapter 3 Manipulating HTML Elements
Remember, you won’t actually see this image yet. It will need to be
added to the document later.
There are two particular properties that you might add, especially for
the benefit of CSS:
» img.id = 'something'; // id="..."
» img.classList.add('whatever'); // class="...""
» img.className = 'whatever'; // older alternative
Note that for the most part, the JavaScript property name is the same
as the HTML attribute, but the class attribute doesn’t match exactly, since
the word class is a reserved word in JavaScript.
There are two ways of adding a class to an element:
113
Chapter 3 Manipulating HTML Elements
• existingElement.before(newElement): Outside,
before the existing element
114
Chapter 3 Manipulating HTML Elements
If you look at existing code, you may see the appendChild() function
used often. This is nearly the same as the newer append() function. There
are some differences, but most of the time, you can use either.
For example, to add a new image after an existing heading, you
could write
» var h1 = document.querySelector('h1');
» h1.after(img);
There is also an alternative single function that will do all four of the
mentioned functions, given an additional parameter with a position:
existingElement.insertAdjacentElement(position,newElement). The
position is one of the following:
115
Chapter 3 Manipulating HTML Elements
» var h1 = document.querySelector('h1');
» h1.insertAdjacentElement('afterend',img);
That certainly isn’t more convenient, but it may appeal to your sense of
pattern.
» var h1 = document.querySelector('h1');
» var html = '<img src="..." alt="...">';
» h1.insertAdjacentHTML('afterend',html);
116
Chapter 3 Manipulating HTML Elements
» var h1 = document.querySelector('h1');
» h1.insertAdjacentHTML('afterend', '<img src="..."
alt="...">');
» document.querySelector('h1')
» .insertAdjacentHTML('afterend', '<img src="..."
alt="...">');
» var h1 = document.querySelector('h1');
» var ul = '<ul><li>one</li><li>zwei</li><li>trois
</li></ul>';
» h1.insertAdjacentHTML('afterend',ul);
As you know, JavaScript has a new type of string literal, the template
literal. We’ve been mainly using it to interpolate values using the ${...}
notation. However, the other feature is that this string literal can be written
over multiple lines:
» var h1 = document.querySelector('h1');
» var ul =
117
Chapter 3 Manipulating HTML Elements
`<ul>
<li>one</li>
<li>zwei</li>
<li>trois</li>
</ul>`;
» h1.insertAdjacentHTML('afterend',ul);
Since the string is treated as HTML, the spacing won’t make any
difference to the result, since additional spacing and line breaks are
ignored in HTML. However, it might make the code easier to read, and
there are other occasions where the spacing and line breaks are significant.
As before, HTML strings are fine if all you want to do is add the
elements, but you won’t get a reference to them. If, on the other hand,
you need to refer to them later in JavaScript, you will need to create the
elements individually:
» var h1=document.querySelector('h1');
» h1.insertAdjacentElement('afterend',message);
» h1.insertAdjacentElement('afterend',heading);
» heading.textContent = 'Message';
» message.textContent = `The Time is ${new Date()}`;
In the sample code, it made sense to give the heading and paragraph
more meaningful names, as the code will assign them content later in the
script. Note that the elements are added in the reverse order, since they
insert immediately after the h1 element.
This is the sort of thing we’ll be doing in the next project in Chapter 4,
on the slide show.
118
Chapter 3 Manipulating HTML Elements
Summary
In this chapter, we’ve looked at some of the techniques used in
manipulating a web page.
Finding Elements
There are many functions available to find existing elements in the
DOM. Some of these have been available for some time.
Modern browsers all support the newer functions querySelector()
and querySelectorAll(), which greatly simplifies locating elements.
These functions use CSS selectors to describe the element(s) you’re
looking for.
Once an element is located, there are functions for referencing other
nearby elements in the DOM tree: parents, siblings, and children.
Multiple Elements
Some functions, especially querySelectorAll(), return a collection of
elements, even if the collection is empty or has one member only.
119
Chapter 3 Manipulating HTML Elements
To handle a collection, you can use the classical for( ... ; ... ;
... ) loop, or use the more modern forEach() function to iterate through
the elements.
C
reating Elements
JavaScript can also create new elements. You can even create elements
that have no meaning in HTML, as long as you don’t expect the browser to
know what to do next.
With a newly created element, you can add content as well as
properties, which match HTML attributes. Using the same methods, you
can also do this to existing elements located earlier.
New elements need to be added to the DOM using one of a number
of functions. These functions place an element near another existing
element.
You can also add HTML directly, either to an existing element, using
innerHTML, or in creating a new element, using insertAdjacentHTML().
C
oming Up
We’ve looked at using JavaScript to make changes to a web page. In the
next chapter, we’re going to apply that to creating an image slide show.
In the next project, we’ll look at adding additional elements to a page,
changing the behavior of existing elements, and interacting with CSS
styles. We’ll also learn about working with JavaScript Arrays and Objects.
Finally, we’ll learn about how JavaScript can respond to keyboard and
mouse events.
120
CHAPTER 4
The slide show works by periodically changing the src property of the
main image.
In this project, we will develop techniques in
• Locating elements
• Arrays
• Preloading images
• Modulus arithmetic
122
Chapter 4 Creating a Slide Show
One thing you will see later is the use of CSS for certain animation
effects. CSS is the coding language that defines the appearance of HTML
elements, and you can use JavaScript to create and apply additional CSS.
If you’re familiar with CSS, that’s helpful, but it’s not necessary; you’ll
just need a little faith. Just be aware that modern CSS can be used for all
sorts of animation tricks which once relied on additional JavaScript. This
leaves you free to focus on implementing the logic of your project, letting
CSS handle the visual aspects.
The src attribute is the URL of the actual image. HTML doesn’t
actually contain the image but tells the browser where to get it from.
Normally, the image is local to the website, but it can be a remote image if
the URL includes the http:// or https:// protocols.
When the browser encounters an img tag, it will send off a request
for the image referenced in the src attribute. Generally getting the image
takes some time, so the browser continues processing the rest of the
HTML. When an image arrives from the server, the browser will display it
in the space reserved for it.
If the image can’t be viewed – such as when original is missing, or takes
too long to arrive, or when the user is visually impaired and relies on text –
then the browser will use the alt text instead.
123
Chapter 4 Creating a Slide Show
The title is additional text that may be displayed when the mouse
hovers over the image; it’s also helpful for search engines to describe
the image.
The width and height attributes are there to help the browser create a
suitable space for the image while it is waiting for it to arrive. They can also
be used to resize an image.
Our slide show will cycle through a collection of images by changing
the value of the src attribute, causing the browser to reload a new image.
The whole of the slide show will be contained in the following
structure:
<div id="slides">
<img width="640" height="480" src="" title="..." alt="...">
</div>
<head>
<title>Slide Show</title>
...
<script type="text/javascript" src="scripts/library.js"
crossorigin="anonymous"></script>
<script type="text/javascript" src="scripts/slideshow.js"
crossorigin="anonymous" defer></script>
</head>
124
Chapter 4 Creating a Slide Show
As with the other projects, the slideshow.js file will contain the main
code. However, the actual code for running the slide show is something
that you might want to use in other projects; you will certainly want it for
the slide show variant later in the book. For that reason, this code will be
placed in the library.js file for reuse.
Also note that the library doesn’t need the defer attribute, as it won’t
interfere with or rely on the page as it’s loading.
/* Slide Show
================================================
================================================ */
'use strict';
window.onerror = function(message, url, line) {
alert(`Error: ${message}\n${url}: ${line}`);
};
init();
function init() {
}
125
Chapter 4 Creating a Slide Show
In the library file, we can add a function to do all of the hard work. This
function needs to be given the collection of images and where to put them:
/* doSlides(images, containerSelector)
================================================
Turns a container element into an slide show
with a collection of images.
================================================ */
}
» console.log(fruit.length);
4
Note that since the index starts at 0, the length is one more than the
last index.
126
Chapter 4 Creating a Slide Show
For our project, we will use an array of image names. We’ll put that into
the init() function in the slideshow.js file. We’ll then send the variable
to the doSlides() function.
function init() {
let images = [
'bamboo.jpg', 'bird.jpg', 'bromeliad.jpg',
'bush-panorama.jpg', 'canal.jpg', 'church.jpg',
'cityscape.jpg', 'emu.jpg', 'gum-nuts.jpg',
'hanging-rock.jpg', 'hut.jpg', 'koala.jpg',
'kookaburra.jpg', 'lake.jpg', 'lantern.jpg',
'light-house.jpg', 'lorikeet.jpg', 'mountain-panorama.jpg',
'rocks.jpg', 'rose.jpg', 'steps.jpg',
'sunset.jpg', 'tall-trees.jpg', 'tea-leaf.jpg',
'train.jpg','waterfall.jpg'
];
}
That’s a lot of images, and we had to resort to writing the list over
multiple lines. In some cases, it might make more sense to write one item
per line.
If you don’t feel like typing it in, there’s a snippet for this array.
Of course, this isn’t really a collection of images. It’s a collection of file
names, and we’ll need to turn that into actual images later in the code by
adding the images/slides/ folder to the file names and putting that into
the src attribute.
You might decide that asking JavaScript to manage such a large
collection is risky or inconvenient. Later we will look at how JavaScript can
read this list from an external file, using Ajax.
127
Chapter 4 Creating a Slide Show
Using this collection, you will invoke the doSlides() function in the
library, giving it the images collection and a reference to the container:
// slideshow.js
function init() {
let images = [...];
doSlides(images, 'div#slides');
}
The div#slides string is the same as the CSS selector and means
“the div whose id is slides.” JavaScript will then use this string in the
document.querySelector() function to locate the actual element.
// library.js
function doSlides(images, containerSelector) {
// Elements
let container = document.querySelector(containerSelector);
let img = container.querySelector('img');
}
128
Chapter 4 Creating a Slide Show
As the images array has only the file names, we’ve added the path of
these images, relative to the root of the website. This makes the code more
flexible, since we can easily change the location of the images without
changing the collection itself.
If you reload the page, you should see the image.
Since we have a whole collection of images, we should be using one of
them from the array:
129
Chapter 4 Creating a Slide Show
Notice the different quote marks (` ... `), the so-called backticks.
This type of string is referred to as a template literal. For normal text, the
effect is the same as single or double quotes, but template literals allow you
to embed values and calculations inside the ${...} syntax.
Here, we have hard-coded the path but interpolated the actual image
file name.
If you reload the page, you should see the same image. At least you’ll
see that it’s working.
Of course, you will want to set the img.src repeatedly for each new
image, so it is best to put this inside a function:
next();
function next() {
// Populate Image
img.src = `images/slides/${images[0]}`;
}
}
setTimeout(next, 3000);
// next();
...
}
130
Chapter 4 Creating a Slide Show
131
Chapter 4 Creating a Slide Show
// Variables
let slideNumber = 0;
// setTimeout(next, 3000);
setInterval(next, 3000);
// next()
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber]}`;
slideNumber++;
}
}
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber++]}`;
}
132
Chapter 4 Creating a Slide Show
If you run this version, it will work, but you will eventually see a new
problem. (If “eventually” is too long to wait, you can shorten the delay to,
say, 1000 to see the problem sooner.)
The code will keep going past the end of the array. The value of
slideNumber will keep incrementing beyond the upper limit of the array,
so you will get undefined from there on. You won’t actually see undefined,
but you will see that the image is now missing.
This will require resetting the slideNumber when you get to the end:
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber++]}`;
// Wrap Around
if(slideNumber >= images.length) slideNumber = 0;
}
Remember that the length of an array is one more than the last index,
so when you reach that value (slideNumber == images.length), you
have already gone too far. As a matter of policy, you should always check
whether you have gone even further (slideNumber >= images.length) in
case you missed it before. For example, you already know that repeatedly
adding 0.1 doesn’t always result in a simple result.
This will do the job, but there are two improvements you can easily
make. First, there is a delay before the first image is displayed, and second,
the delay between slides should be more configurable.
133
Chapter 4 Creating a Slide Show
This will now start with the first slide, with subsequent slides cycling
through.
// slideshow.js
function init() {
let images = [...];
doSlides(images, 'div#slides', 3000);
}
134
Chapter 4 Creating a Slide Show
// library.js
function doSlides(images, containerSelector, delay=3000) {
...
}
For testing purposes, you might speed up the slide show by using a
short delay value, such as with doSlides(images, 'div#slides', 1000);
or even shorter.
135
Chapter 4 Creating a Slide Show
136
Chapter 4 Creating a Slide Show
137
Chapter 4 Creating a Slide Show
function toggle() {
running = setInterval(next, delay);
next();
}
function next() {
...
}
}
That’s not so true if you’re writing code in the console. Because the
console has no way of knowing what you’re going to type next, you
need to define whatever you’re going to refer to first.
138
Chapter 4 Creating a Slide Show
The role of the toggle() function is to start the slide show if it’s not
already running, or to stop it if it is. That will require an if ... else:
function toggle() {
if(!running) {
running = setInterval(next, delay);
next();
}
else {
}
}
Remember that if() will test for a true value, but also for a
“something” value such as a nonzero number, or a non-empty string. The
special undefined value definitely counts as “nothing,” so !running would
test if it is undefined.
To stop the slide show, you use the clearInterval() function, which
takes as its parameter the interval id previously saved. You will also have to
reset the running value for next time.
function toggle() {
if(!running) {
...
}
else {
clearInterval(running);
running = undefined;
}
}
139
Chapter 4 Creating a Slide Show
function toggle() {
if(!running) {
...
}
else {
running = clearInterval(running);
}
}
At this stage, if you test the code, you will find that, so far, clicking on
the image will do nothing.
If you try this, the slide show won’t have started yet, since it’s now
waiting for you to click on the image.
140
Chapter 4 Creating a Slide Show
The onclick event listener property will trigger when you click the
mouse on the element, or touch on the element on a touch screen. Again
you use toggle function without parentheses, since you are referring to the
function, but not (yet) calling it.
141
Chapter 4 Creating a Slide Show
At this point, the slide show will load the first image, by virtue of the
next() function call. It will then wait for the user to click on the image
to start it. If you feel that it should start immediately, you can replace the
next() function call with toggle():
Here, the next() function call is commented out rather than deleted.
This way, it is easy to choose between the two behaviors.
142
Chapter 4 Creating a Slide Show
If you want to eliminate waiting for the image the first time, the trick
is to fetch the image before we need it, so it can be added to the cache for
when we need it. This is called prefetch.
To prefetch an image, we will first create an image object. This object
will not be displayed but simply exist in memory. As we display one image,
we will also populate this new object with the next image in the array. The
delay in loading the image will be absorbed while the user is looking at the
current image.
To create a new image object, we call a special function called Image():
143
Chapter 4 Creating a Slide Show
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber++]}`;
// Wrap Around
if(slideNumber>=images.length) slideNumber = 0;
// Prefetch
prefetch.src = `images/slides/${images[slideNumber]}`;
}
144
Chapter 4 Creating a Slide Show
// Variables
...
}
145
Chapter 4 Creating a Slide Show
The textContent property sets what you see on the page. We will
change it as we pause or resume the slide show.
The id isn’t strictly necessary, as we won’t need it to make it work.
However, if you want any control over its appearance, you will need to
target it in CSS. The attached CSS file has style rules for button#run-
button; you can, of course, use any id you like, as long as you modify your
CSS accordingly.
The whole point of the button is to pause and resume the slide show,
so we will attach the event listener as we did with the image:
img.addEventListener('click', toggle);
button.addEventListener('click', toggle);
...
}
146
Chapter 4 Creating a Slide Show
In our case, we will add the button inside the container at the end:
If you don’t see the new button clearly, move your mouse over a
faint image of it at the bottom right corner of the container. The CSS is
responsible for placing it over the image, dimming it out, and highlighting
it when the mouse is over. The relevant part of the CSS looks a little
like this:
button#run-button {
...
position: absolute;
bottom: 1em; right: 1em;
opacity: .2;
transition: opacity 500ms;
}
button#run-button:hover {
opacity: 1;
}
At this point, you might decide that you don’t need the whole image to
act as a button:
// img.addEventListener('click', toggle);
147
Chapter 4 Creating a Slide Show
function toggle() {
if(!running) {
running = setInterval(next, delay);
next();
button.textContent = 'Stop';
}
else {
running = clearInterval(running);
button.textContent = 'Start';
}
}
148
Chapter 4 Creating a Slide Show
function toggle() {
if(!running) {
running = setInterval(next, delay);
next();
button.textContent = 'Stop';
container.classList.add('running');
}
else {
running = clearInterval(running);
button.textContent = 'Start';
container.classList.remove('running');
}
}
div#slides {
outline: medium solid #666;
}
div#slides.running {
outline-color: black;
}
149
Chapter 4 Creating a Slide Show
You will see that CSS will change the color of the border when the slide
show is paused or running.
Responding to a Keypress
We have added a button to pause and resume the slide show, but it would
be just a little more convenient if we could do that with a keypress. Here,
we can use the space bar.
If you have form input or a textarea element, you can set up an event
listener to respond to particular key strokes within that element. In this
case, we want the whole document to respond to the space bar, so we will
assign an event listener to the document itself:
document.onkeypress = ...;
The event listener is for the keypress event, and here we can use the
simple form. As mentioned before, an event listener is always a function,
such as the toggle function. In this case, we will need a different function
to first test which key was pressed and then call the toggle function.
So far, we’ve created a function by name and assigned the function as
an event listener, something like this:
button.onclick = toggle;
function toggle() {
Defining the function this way is useful if you’re going to use it again
later. However, you can do the same sort of thing without defining the
function separately:
button.onclick = function() {
150
Chapter 4 Creating a Slide Show
However, the name would only be available within the function itself,
so you won’t see this pattern very much.
For the keypress, we’ll use the same idea to create our event listener
without creating a separate function:
document.onkeypress = function(event) {
};
function toggle() {
...
}
...
}
Note that the function also has a parameter. All event listeners are
given a special event object as a parameter, but in many cases, you won’t
bother with it. You can call it anything you like, but you will commonly see
it named as event, e, or even evt.
JavaScript allows you to ignore incoming parameter values, as we have
done so far. This time, however, we will need it.
151
Chapter 4 Creating a Slide Show
The event object contains all sorts of information about the event,
such as where the mouse was when it happened, whether the shift key was
also pressed, and which key triggered the event. The key that was pressed
is in the .key property, and we can test it with a simple if():
document.onkeypress = function(event) {
if(event.key == ' ') {
}
};
Here, we’re only interested in the space bar, so we just use a string with
a space (' ').
At this point, if the test passes, we can call the toggle:
document.onkeypress = function(event) {
if(event.key==' ') {
toggle();
}
};
However, the browser typically has its own idea of how to respond to
certain keys, and that might interfere with our own plans. We can hijack
the behavior entirely by telling the browser not to continue responding
with the event.preventDefault() function:
document.onkeypress = function(event) {
if(event.key==' ') {
toggle();
event.preventDefault();
}
};
152
Chapter 4 Creating a Slide Show
JavaScript Objects
So far, the images array contains items with image names only. If we want
to include captions, each item will need to be a little more complex.
An object is a package of data. JavaScript has many predefined objects,
but you can make your own object by defining a collection of object
properties:
153
Chapter 4 Creating a Slide Show
» var apple = {
shape: 'round',
colour: 'red'
};
This is called an object literal, as you are defining the properties in the
code itself. In JavaScript, an object literal is contained inside braces ({ ... }),
which looks just like a block of code but isn’t. In fact, the fact that braces are
used for two different things can lead JavaScript to misinterpret some code.
Each property of the object can be read as object.property:
» console.log(apple.colour);
» apple.colour = 'green';
apple.Colour = 'green';
If you’re going to make many objects with the same properties but
different values, you might create a constructor function to do it for you:
{
src: '...',
caption: '...'
}
where the src property is the name of the image and the caption is what
it says.
We can call this new array images2, to distinguish it from the original
collection.
By writing each object on one line, the images array would now be
let images2 = [
{ src: '...', caption: '...' },
{ src: '...', caption: '...' },
{ src: '...', caption: '...' }
];
You can now define the images array in the sample project as
let images2 = [
{ src: 'bamboo.jpg',
caption: 'Bamboo Forest In Japan' },
{ src: 'bird.jpg', caption: 'Bird in Flight' },
{ src: 'bromeliad.jpg', caption: 'Bromeliad Flower' },
{ src: 'bush-panorama.jpg',
155
Chapter 4 Creating a Slide Show
156
Chapter 4 Creating a Slide Show
To save you too much typing, there’s also a snippet for this array.
In the slideshow.js file, you can change the function call to use the
images2 array:
function next() {
// Populate Image
// img.src = `images/slides/${images[slideNumber++]}`;
img.src = `images/slides/${images[slideNumber++].src}`;
// Wrap Around
if(slideNumber>=images.length) slideNumber = 0;
// Prefetch
// prefetch.src = `images/slides/${images[slideNumber]}`;
prefetch.src = `images/slides/${images[slideNumber].src}`;
}
157
Chapter 4 Creating a Slide Show
// Add Caption
let caption = document.createElement('p');
container.insertAdjacentElement('beforeend', caption);
// Variables
...
}
Here, the paragraph is added at the end of the container, after the
previously added button. The CSS is responsible for locating the new
paragraph in the container and defining its appearance, such as centering
and bold-facing the text.
All that remains is for the next() function to populate the caption:
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber].src}`;
caption.textContent = images[slideNumber++].caption;
...
}
158
Chapter 4 Creating a Slide Show
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber].src}`;
caption.textContent = images[slideNumber].caption;
slideNumber++;
...
}
At the same time, you can also set the title property of the image
itself, as well as the alt property, which you can set to the same:
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber].src}`;
caption.textContent = images[slideNumber].caption;
img.title = images[slideNumber].caption;
img.alt = images[slideNumber].caption;
slideNumber++;
...
}
The title property of an image is usually not displayed but will show
as a tool tip if you hover the mouse over it.
159
Chapter 4 Creating a Slide Show
Summary
In this chapter, we looked at techniques for manipulating the contents of
a web page, using an image slide show. In the process, we developed a
number of skills.
Organizing Code
We put the actual slide show code in the library.js file so that it can
be reused. The function also accepts a collection of images and the CSS
selector of the slide show container to make it as flexible as possible.
Locating Elements
We used the .querySelector() function to locate the slide container as
well as the img element inside it.
Arrays
An array is a numbered collection of items. You can create an array from an
array literal:
Arrays are numbered from 0. You can get the number of items using
the .length property. You can get an individual item from an array using
an index in square brackets: fruit[1].
160
Chapter 4 Creating a Slide Show
The browser will immediately attempt to reload the image once the
.src property has been changed.
161
Chapter 4 Creating a Slide Show
Objects
We proceeded to including a caption for each image. This required
modifying the array from an array of strings to an array of objects.
An object is a collection of values. The objects used for the images
included a .src property and a .caption property.
We then refactored the code to use the .src property of each object.
We also created a new paragraph element for the caption and
populated it with the caption property of each object.
Coming Up
The next project will involve interacting with a web form. That will be in
Chapter 6, on creating a specialized calculator.
Before that, however, we’ll learn more about how forms work and how
we work with them.
162
CHAPTER 5
164
Chapter 5 Working with Forms
value="[email protected]">
</label></p>
<p><label>Subject:<br>
<input type="text" name="subject"
value="General Enquiry">
</label></p>
<p><label>Message:<br>
<textarea name="message">This space ...</textarea>
</label></p>
</fieldset>
<p><button type="submit" name="send">Send Message
</button></p>
</form>
165
Chapter 5 Working with Forms
The actual appearance will depend partly on the browser and very
much on CSS styles. Modern developers put a lot of effort into making
forms look good and easy to use. The appearance has no effect whatever
on what the web server sees when the data has been submitted; however, it
will certainly affect how the user interacts with the form.
The important parts of the sample form are as follows:
166
Chapter 5 Working with Forms
Apart from other HTML attributes, form field elements have a few
relevant attributes:
167
Chapter 5 Working with Forms
168
Chapter 5 Working with Forms
The Get data is the data that was added to the URL in the form of a
query string. The Post data is the data from the form itself, in which the
method was set to post. If the method was set to get, it would have been
added to the query string as more Get data.
169
Chapter 5 Working with Forms
The first thing you will need is a reference to the form. Assuming that
the form has an id, you can use something like this:
170
Chapter 5 Working with Forms
The last way is the one we’ll prefer as it’s the safest and works for all
valid names.
If you run the code shown previously, you will see the element itself,
but not the text in the element. For that, you will need to read some of the
element’s properties.
171
Chapter 5 Working with Forms
Note that the .value property is always a string. If you are expecting
a number, you may have to run it through parseInt() or parseFloat()
to convert it to a number. This is the same problem you will have
encountered with the prompt function in the Guessing Game.
form.elements['send'];
172
Chapter 5 Working with Forms
}
» form.elements['send'].onclick = doSomething;
173
Chapter 5 Working with Forms
function doSomething(event) {
event.preventDefault();
// process form
}
If you don’t cancel the submit, then the form will be submitted, and in
this case, the page will be reloaded.
Summary
In this chapter, we had a quick look at HTML forms. We looked at
174
Chapter 5 Working with Forms
Coming Up
Now that we know about forms and JavaScript, we’re going to use a form as
a customized calculator.
In the calculator project, we’ll learn more about working with forms
and form fields and processing data from the form.
We’ll also learn more about techniques in processing and calculating
with data and a little bit about storing data for the future.
175
CHAPTER 6
Creating a Custom
Calculator Form
Web forms are a standard method of allowing the user to communicate
back to the web server. This is often to send messages or to place orders.
Although forms will work as they are, they are often supplemented with
JavaScript to preprocess data and to check over it.
In this project, we will use JavaScript to process data without sending it
to the server. In this case, we’ll create a specialized calculator.
In developing this project, we’ll learn about the following:
• Coding a formula
(1 + r )
n
pr
(1 + r ) − 1
n
where
• p = principal
• n = number of payments
This will do the job, but two of the values are not very user-friendly.
When calculating mortgages, we normally express the interest rate per
year, not the rate per payment. And we normally work with the number of
years, not the number of payments.
178
Chapter 6 Creating a Custom Calculator Form
<form id="data">
<fieldset>
<p><label>Amount to be Borrowed:<br>
<input type="text" name="principal"
value="300000"></label></p>
<p><label>Term in Years:<br>
<input type="text" name="term" value="30">
</label></p>
<p><label>Frequency of Payment:<br>
<input type="text" name="frequency"
value="12"></label></p>
<p><label>Interest Rate (% pa):<br>
<input type="text" name="rate" value="6">
</label></p>
</fieldset>
<fieldset id="results">
<p><output name="result"></p>
<p><button name="calculate">Tell Me</button></p>
</fieldset>
</form>
179
Chapter 6 Creating a Custom Calculator Form
In this form, the two important input element types are <input
type="text" name="..."> and <button name="...">, which are the text
boxes and action button.
You’ll see that there’s also an output element. That’s a simple element
into which you can write whatever you like in JavaScript. It will be part of
the elements collection of the form, but if you were to submit the form, it
wouldn’t be included.
To implement the calculator, we will respond to the button, read and
the process from the input elements, and place the result in the output
element.
180
Chapter 6 Creating a Custom Calculator Form
Note that these are all text type inputs. HTML does have a
type="number", but it’s not so flexible. In any case, the data will be a string,
even if we were to use type="number", so there’s not much benefit for
us there.
The four values are the sorts of values a person would expect to fill in,
but the formula wants something different. To prepare the values for the
formula:
number of payments
= term in years * payments per year
Note that we’ve made another concession to the user. We’re used to
expressing interest rates as a percentage, but JavaScript doesn’t know
181
Chapter 6 Creating a Custom Calculator Form
anything about percentages. So we’ll let the user type in what looks like a
percentage, and we’ll divide by 100 in the code.1
<!DOCTYPE html>
<html lang="en">
<head>
...
<script type="text/javascript" src="scripts/library.js"
crossorigin="anonymous"></script>
<script type="text/javascript" src="scripts/calculator.js"
crossorigin="anonymous" defer></script>
...
</head>
<body>
...
</body>
</html>
Notice that this includes the library script. If some of the new code is
reusable, we may want to move it to the library.
The calculator.js file has already been created for your convenience
and is ready to go:
/* Mortgage Calculator
1
JavaScript does support the % operator, but it doesn’t mean anything like
percentage. It’s used as a remainder after division.
182
Chapter 6 Creating a Custom Calculator Form
================================================ */
'use strict';
window.onerror=function(message,url,line) {
alert(`Error: ${message}\n${url}: ${line}`);
};
doCalculator();
function doCalculator() {
}
function doCalculator() {
let calculatorForm = document.querySelector('form#data');
}
Having identified the form, you will need to attach an event listener to
the submit button:
function doCalculator() {
let calculatorForm = document.querySelector('form#data');
calculatorForm.elements['calculate'].onclick = doit;
function doit(event) {
}
}
183
Chapter 6 Creating a Custom Calculator Form
Here, the submit button has the name calculate. The event listener
is the doit function. You might not think that doit is a very descriptive
name, but we can justify its simplicity.
In this case, the doit function is nested inside the doCalculator()
function. As a result, this function has a number of features:
The doit function includes the event parameter, which we can use to
stop the form from actually being submitted:
function doit(event) {
event.preventDefault();
If you forget to do this, you will end up reloading your page every time
you click the submit button, and you will never see your result.
184
Chapter 6 Creating a Custom Calculator Form
function doit(event) {
event.preventDefault();
function doit(event) {
event.preventDefault();
let principal =
calculatorForm.elements['principal'].value.trim();
let term = calculatorForm.elements['term'].value.trim();
let frequency =
calculatorForm.elements['frequency'].value.trim();
let rate = calculatorForm.elements['rate'].value.trim();
}
185
Chapter 6 Creating a Custom Calculator Form
The first method uses the isNaN() function, which you need to
remember has two capital Ns. The second method exploits an odd feature
that NaN is not equal to anything, including NaN. You wouldn’t use this
method seriously, but you might like to show off.
186
Chapter 6 Creating a Custom Calculator Form
In reality, you can just take advantage of the fact that NaN is treated as a
falsy value, so you can just use the not (!) operator:
This way, you can assign a default value if the string fails parsing.
In JavaScript, you can combine the last two steps shown previously
using the || (or) operator:
function doit(event) {
event.preventDefault();
let principal =
calculatorForm.elements['principal'].value.trim();
principal = parseFloat(principal);
if(!principal) principal = 0;
let term = calculatorForm.elements['term'].value.trim();
term = parseFloat(term);
if(!term) term = 0;
let frequency =
calculatorForm.elements['frequency'].value.trim();
187
Chapter 6 Creating a Custom Calculator Form
frequency = parseFloat(frequency);
if(!frequency) frequency = 1;
let rate = calculatorForm.elements['rate'].value.trim();
rate = parseFloat(rate);
if(!rate) rate = 0;
}
or more simply:
function doit(event) {
event.preventDefault();
let principal =
calculatorForm.elements['principal'].value.trim();
principal = parseFloat(principal) || 0;
let term = calculatorForm.elements['term'].value.trim();
term = parseFloat(term) || 0;
let frequency =
calculatorForm.elements['frequency'].value.trim();
frequency = parseFloat(frequency) || 0;
let rate = calculatorForm.elements['rate'].value.trim();
rate = parseFloat(rate) || 0;
}
principal = parseFloat(principal) ||
parseFloat(calculatorForm.elements['principal'].
defaultValue);
188
Chapter 6 Creating a Custom Calculator Form
function doit(event) {
event.preventDefault();
let principal =
calculatorForm.elements['principal'].value.trim();
principal = parseFloat(principal) || 0;
calculatorForm.elements['principal'].value = principal;
let term = calculatorForm.elements['term'].value.trim();
term = parseFloat(term) || 0;
calculatorForm.elements['term'].value = term;
let frequency =
calculatorForm.elements['frequency'].value.trim();
frequency = parseFloat(frequency) || 0;
calculatorForm.elements['frequency'].value = frequency;
let rate = calculatorForm.elements['rate'].value.trim();
rate = parseFloat(rate) || 0;
calculatorForm.elements['rate'].value = rate;
}
189
Chapter 6 Creating a Custom Calculator Form
You can try this out now with invalid values. Any defaulted values
will be written back to the form, though, at this stage, nothing will be
calculated yet.
(1 + r )
n
pr
(1 + r ) − 1
n
The first thing you will have to do is to prepare the values for the value:
• The principal is straightforward, and we will use the
value we have got from the form, principal. The
only thing is that we might put it in a new variable for
simplicity:
let p = principle;
190
Chapter 6 Creating a Custom Calculator Form
function doit(event) {
event.preventDefault();
...
let p = principal;
let n = term * frequency;
let r = rate / frequency / 100;
}
p * r * ...
(1 + r)**n
something
• The expression means you will have to
something
divide. However, the lower part of the expression
is an addition, which you will need to enclose in
parentheses.
p * r * (1+r)**n / ((1+r)**n - 1)
191
Chapter 6 Creating a Custom Calculator Form
p * r * Math.pow(1+r,n) / (Math.pow(1+r,n) - 1)
function doit(event) {
...
function doit(event) {
...
192
Chapter 6 Creating a Custom Calculator Form
function doit(event) {
...
let repayment = p * r * (1+r)**n / ((1+r)**n - 1);
calculatorForm.elements['result'].value = repayment.
toFixed(2);
}
If you try this now with the default values, you should get a result of
1798.65.
Improvements
There are a few things we can do to make this calculator form a little better.
Here, we’ll do just these two things:
It’s possible to get carried away with minor improvements, but these
particular ones are not too difficult to implement.
193
Chapter 6 Creating a Custom Calculator Form
Here, the locale is set to en-AU, which means English in Australia. The
important part here is that the locale helps define that the decimal is a
dot (.) and thousands are separated by a comma (,). If you’re doing this
in Europe, say, you might want to use something like nl-NL, which is the
format used in the Netherlands: the decimal and thousands characters are
the other way round.
The second parameter is an options object. Here, we’re saying that
we want the number displayed as currency, using Australian Dollars. The
currency and the locale don’t have to match. If you want Australian Dollars
displayed in the Netherlands number format, that’s fine:
194
Chapter 6 Creating a Custom Calculator Form
Formatting a Number
Of course, you won’t get a formatted number yet. For that, you’ll need the
format() method of the object:
» numberFormat.format(123456.789);
← "$123,456.79"
Notice that the number has been rounded off correctly to two decimal
places. Also notice that the result is a string. Numbers have a value only
and don’t include currency symbols or thousands separators. If you want
that sort of embellishment, you need a string, which is fine for what we’re
going to do with it.
If you’re going to use the formatter many times, you can make it a little
more convenient if you include the format() method in the variable:
function doCalculator() {
let calculatorForm = document.querySelector('form#data');
calculatorForm.elements['calculate'].onclick = doit;
195
Chapter 6 Creating a Custom Calculator Form
function doit(event) {
...
}
}
We’ve changed the var to let because we’re not in the console
anymore.
The doit() function is, of course, run every time you click on the
calculate button. As with the calculatorForm variable, it has access to
the format() function from the containing function.
We won’t need the toFixed() method anymore. That worked well
enough, but the formatting function also rounds off the result. We can
replace it with the following:
function doit(event) {
...
let repayment = p * r * (1+r)**n / ((1+r)**n - 1);
// calculatorForm.elements['result'].value =
// repayment.toFixed(2);
calculatorForm.elements['result'].value =
format(repayment);
}
196
Chapter 6 Creating a Custom Calculator Form
197
Chapter 6 Creating a Custom Calculator Form
198
Chapter 6 Creating a Custom Calculator Form
They both work the same way, so all you need to decide is how long
you want the data to hang around. In this case, we’ll use “localStorage”.
The storage object has a number of methods to set and get data. Here,
we’ll store the values we have just used to calculate the result:
function doit(event) {
...
// Store Values
localStorage.setItem('data', true);
localStorage.setItem('principal', principal);
localStorage.setItem('term', term);
localStorage.setItem('frequency', frequency);
localStorage.setItem('rate', rate);
}
The actual names of the items can be whatever you like, of course, but
here we’ve set them to the same names as the variables and the original
form data.
We’ve also set another item, data. This will make it easy to test whether
any data has been stored when we load the page the next time.
You can check what your browser has in storage in the Developer
Tools. For Firefox and Safari, you’ll find it in a Storage section. In
Chromium, there’s a Storage section in the Application section. You’ll see
something like this:
Key Value
data true
frequency 12
principal 300000
rate 6
term 30
199
Chapter 6 Creating a Custom Calculator Form
function doCalculator() {
...
// Restore Values
if(localStorage.getItem('data')) {
}
function doit(event) {
...
}
}
function doCalculator() {
...
// Restore Values
if(localStorage.getItem('data')) {
calculatorForm.elements['principal'].value =
localStorage.getItem('principal');
calculatorForm.elements['term'].value =
200
Chapter 6 Creating a Custom Calculator Form
localStorage.getItem('term');
calculatorForm.elements['frequency'].value =
localStorage.getItem('frequency');
calculatorForm.elements['rate'].value =
localStorage.getItem('rate');
}
...
}
The good thing about web storage is that it’s not only easy to use, it’s
also secure. At least it’s as secure as your browser is: if others can come up
to your browser, they can still examine what was stored. For that reason,
you should never store anything really sensitive this way.
At least it won’t be sent back to the web server.
['principal','term','frequency','rate']
To iterate through an array, we can use a counter loop. You can try this
in the console:
201
Chapter 6 Creating a Custom Calculator Form
As you see, the .forEach() method takes a function with one or more
parameters. The first, and most useful, parameter is each item of the array.
While we’re using modern JavaScript, it’s also common to use an arrow
function expression:
» ['principal','term','frequency','rate'].forEach(item => {
console.log(item);
});
202
Chapter 6 Creating a Custom Calculator Form
We can use this to replace the part of the code that restores the values:
// Restore Values
if(localStorage.getItem('data')) {
/*
...
*/
['principal','term','frequency','rate'].forEach(item => {
calculatorForm.elements[item].value =
localStorage.getItem(item);
});
}
The other code has been commented out while you test the new
version. Once you’ve made sure the new code is working, you can delete
the commented code.
Unfortunately, you can’t really do the same thing with the code to store
the values. That’s because the data you want to store is in variables, and
JavaScript has no simple way of iterating through normal variables.
203
Chapter 6 Creating a Custom Calculator Form
» console.log(data);
We can also combine this with the array iteration to list the contents of
the object:
» ['principal','term','frequency','rate'].forEach(item => {
console.log(`${item}: ${data[item]}`);
});
We’ve used the template string literal to include the item name and the
value from the data object.
Notice that we’re using the square brackets notation for the object
members. Remember that it’s an alternative to the dot notation which
allows us to use variables.
In our calculator code, we can replace instances of the four main
variables with object properties. To begin with, we can fetch and check the
data using a .forEach() loop:
function doit(event) {
event.preventDefault();
['principal','term','frequency','rate'].forEach(item => {
data[item] = calculatorForm.elements[item].value.trim();
data[item] = parseFloat(data[item]) || 0;
calculatorForm.elements[item].value = data[item];
204
Chapter 6 Creating a Custom Calculator Form
});
...
}
The variables for the formula can then use these object properties
instead of the old variables:
function doit(event) {
event.preventDefault();
...
let p = data.principal;
let n = data.term * data.frequency;
let r = data.rate / data.frequency / 100;
...
}
function doit(event) {
event.preventDefault();
...
['principal','term','frequency','rate'].forEach(item => {
localStorage.setItem(item, data[item]);
});
}
function doit(event) {
event.preventDefault();
['principal','term','frequency','rate'].forEach(item => {
data[item] = calculatorForm.elements[item].value.trim();
205
Chapter 6 Creating a Custom Calculator Form
data[item] = parseFloat(data[item]) || 0;
calculatorForm.elements[item].value = data[item];
});
let p = data.principal;
let n = data.term * data.frequency;
let r = data.rate / data.frequency / 100;
// Unaffected:
let repayment = p * r * (1+r)**n / ((1+r)**n - 1);
calculatorForm.elements['result'].value =
format(repayment);
// Store Values
localStorage.setItem('data', true);
['principal','term','frequency','rate'].forEach(item => {
localStorage.setItem(item, data[item]);
});
}
206
Chapter 6 Creating a Custom Calculator Form
Summary
In this chapter, we had a look at processing data from a form and storing
the data. To work through the idea, we developed a simple calculator to
calculate mortgage repayments.
207
Chapter 6 Creating a Custom Calculator Form
208
Chapter 6 Creating a Custom Calculator Form
Plain variables can’t be iterated this way, but we can create a simple
object and store values in the object properties instead of using simple
variables. The object properties can be referenced when iterating through
an array of key names.
Coming Up
The next few projects will make some serious changes to what you see on
the page. In some cases, we’ll be creating new HTML elements; in some
cases, we’ll be showing and hiding them; and in some cases, we’ll be
constantly changing the content of these elements.
Before that, we need to learn a little more about manipulating the CSS
styles, as well as how JavaScript responds to user events.
209
CHAPTER 7
You can experiment with both the CSS and JavaScript using your
browser’s development tools. There is a sample file, sample-css.html, for
you to try things out, together with its CSS file sample-css.css.
The sample file looks like Figure 7-1.
212
Chapter 7 Interacting with CSS and Event Listeners
What Is CSS?
The original HTML didn’t include any serious way of controlling the
appearance of page content. An early attempt included the font element;
the less said about that, the better.
Later the CSS language was developed. CSS controls the color, the font
styles, the positioning, and even what is visible. It has developed to include
some visual effects such as shadows, rounded corners, and the ability to
control how solid an object is.
As CSS developed, it also added the ability to animate changes in these
properties, either as simple transitions or as multi-stepped animations.
Some of these features, such as showing and hiding elements or
animating their properties, have, in the past, been accomplished by
JavaScript. In some of the projects, you will have added new elements
to the document. If you keep adding and removing objects, you have a
form of showing and hiding, but at the cost of a lot of extra coding and a
lot of extra work for the browser as it has to cope with new and missing
elements.
Similarly, we created a slide show, which shows one image after
another. If you do this quickly enough, you have an animation, but again
that means a lot of extra coding and work for the browser.
CSS now gives us the tools to do these things without writing them in
JavaScript. What CSS doesn’t do, however, is interact with the user or make
decisions about when and how things happen. That’s where JavaScript
is needed.
213
Chapter 7 Interacting with CSS and Event Listeners
Here, the href attribute refers to the style sheet. In theory, as well
as in practice, the href could be a full URL referencing a different site.
The id isn’t normally needed, but we’ll be able to use it to achieve some
effects later.
Styles can also be included in a style element in the HTML:
<style>
/* style rules ... */
</style>
This is called an inline style and is the most cumbersome way to do it.
It’s also hardest to override since it is applied after the other style sheets.
However, once again, it can be useful to set this property in JavaScript.
Disabling Styles
You can disable a style block or a linked style sheet by setting a disabled
attribute. You normally wouldn’t do this in pure HTML, but you might use
some JavaScript to turn this attribute on and off. That’s one reason why the
previous example includes an id attribute – to allow JavaScript to make
changes to the link.
214
Chapter 7 Interacting with CSS and Event Listeners
For example:
You can activate the style sheet or block again by setting the property
to false:
» link.disabled = false;
Turning a style sheet on and off like this makes it possible, for example,
to respond to user preferences. You might have an additional style sheet
with a different font size or with different colors.
Event Listeners
Interacting with a web page involves setting up event listeners. An event
listener is a function that will run in response to a user-generated event,
such as clicking on something or pressing a key.
You can assign an event listener directly using an event property of the
element. For example:
» function doit() {
console.log(h1.textContent);
}
» var h1 = document.querySelector('h1');
» h1.onclick = doit;
Here, the event property is onclick, which will trigger when the user
has clicked on the element. On touch-enabled devices, it will also respond
to touching, so it’s not necessarily a mouse-only event. When you click on
the heading, or touch it, the function will print out the text of the heading.
215
Chapter 7 Interacting with CSS and Event Listeners
If you want to remove the event listener, you can assign null:
» h1.onclick = null;
» h1.addEventListener('click',doit);
» h1.removeEventListener('click',doit);
There is a trap in removing the event listener this way, which we’ll see
in the section on nested event listeners.
216
Chapter 7 Interacting with CSS and Event Listeners
Event Targets
When you trigger an event listener, you’ll need to know what actually
triggered it.
For example, in the sample file, there is a figure element with an
image and a caption:
<figure>
<img ...>
<figcaption> ... </figcaption>
</figure>
» function doit(event) {
console.log(this);
console.log(event.target);
console.log(event.currentTarget);
}
» var figure = document.querySelector('figure');
» figure.addEventListener('click',doit);
The doit() function includes one parameter, event, which has the
details of the event as well as methods to control it. Inside the function,
there are three important values:
217
Chapter 7 Interacting with CSS and Event Listeners
<figure>
<img> or <figcaption>
<figure>
<figure>
<figure>
<figure>
218
Chapter 7 Interacting with CSS and Event Listeners
It then reaches the target phase, which involves the element you
actually clicked on.
After that, this click is felt from the inside out, from the inner element
to the outer elements. This is called event bubbling and is pictured in
Figure 7-3.
219
Chapter 7 Interacting with CSS and Event Listeners
Most of the time, you’re more interested in the bubble phase, starting
with the clicked element.
When you use the direct onclick property, or the default
addEventListener() method, the event listener will only respond to the
bubble phase.
You can get the event listener to listen to the capture phase using the
addEventListener() with an optional argument:
The third parameter can have other options, but using true indicates
that it should listen to the capture phase; its default is false, which is why
it defaults to the bubble phase.
You can test the bubbling and capturing phase by adding the same
event listener to multiple elements. We’ll do that with the figure and nested
image. For this experiment, we’ll use the .currentTarget, which will tell
us which event listener is responding.
Before we do that, you need to make sure that the current event
listener has been removed:
» figure.removeEventListener('click',doit);
» function doit(event) {
console.log(event.currentTarget);
}
First, add the event listener to the bubble stage. This is the default, but
we’ll make the point by adding false as the last parameter:
220
Chapter 7 Interacting with CSS and Event Listeners
When you click on the image, you’ll see that both elements call the
event listener, but the img element responds first:
<img ...>
<figure>
Now remove the event listeners. When you remove an event listener, it
must match the added event listener in event type (click), event listener
function (doit), and listener type (false). If any of these is different, then
the event listener won’t be successfully removed. Since false is the default
type, you don’t need to specify it, but we’ll do it anyway:
Now, add them with the third argument to true, to call it in the
capture phase:
When you click on the image, you’ll now see the responses in the
reverse order:
<figure>
<img ...>
221
Chapter 7 Interacting with CSS and Event Listeners
Stopping Propagation
You can stop the event function from processing any further. For example,
you may decide that only the innermost element should be processed, or
only the outermost element.
You can stop further processing with the stopPropagation() method.
For example, if you want only the image to be processed, you can use
» function doit(event) {
event.stopPropagation();
console.log(event.currentTarget);
}
» figure.addEventListener('click', doit, false);
» img.addEventListener('click', doit, false);
If you click on the image now, you’ll get only the image, which is
the innermost element, in the output. You can now remove the event
listeners again:
If you wanted only the figure element to respond, you would use the
capture phase by setting the third parameter to true.
» var h1 = document.querySelector('h1');
» h1.style.color = 'red';
222
Chapter 7 Interacting with CSS and Event Listeners
» h1.style.color
← red
However, you can only read a property this way if it was set directly.
Properties set in normal CSS can’t be read this way.
As with inline styles, the style property is applied after all the other
styles, so you can’t override this with other CSS style sheets. This is OK for
some simple properties, but not very flexible, and later you will see how to
cooperate with style sheets better.
The styles property is an object, which is itself a package of data with
its own properties. JavaScript has two ways of addressing a property:
» var h1 = document.querySelector('h1');
The first notation, often called the dot notation for obvious reasons, is
clearly simpler, but it has some limitations. The second notation, the so-
called square brackets notation for equally obvious reasons, overcomes
these limitations.
The first limitation, which is most apparent with CSS, is that the dot
notation doesn’t allow property names, which would be invalid names
in JavaScript. For example, the background-color property includes a
hyphen, which is disallowed in JavaScript since it would be interpreted as
subtraction. Here, the square brackets notation comes to the rescue:
» var h1 = document.querySelector('h1');
223
Chapter 7 Interacting with CSS and Event Listeners
» var h1 = document.querySelector('h1');
» function doit(event) {
if(event.target.style['color'] != 'yellow')
event.target.style['color'] = 'yellow';
else event.target.style['color'] = 'orange';
}
» h1.addEventListener('click',doit);
There is some repetition in this code. We can improve on this, but that
calls for a minor detour.
Resetting Properties
If you want to remove a property set this way, you can set it to null or to an
empty string(''):
224
Chapter 7 Interacting with CSS and Event Listeners
This will allow the element to fall back on the properties set
previously in CSS.
While we’re at it, we should also remove the event listener:
» h1.removeEventListener('click',doit);
Storing State
In the previous example, we tested whether a color had already been
set before deciding what to do with it. Apart from the tediousness, the
technique hard-codes too many values.
A better technique would be to store what we’ve done in a separate
variable. We’ll call that a state variable.
One simple method is to create a variable outside the function and test
and set it inside the function:
» var h1 = document.querySelector('h1');
» var modified = false;
» function doit(event) {
225
Chapter 7 Interacting with CSS and Event Listeners
if(!modified) event.target.style['color']
= 'yellow';
else event.target.style['color'] = null;
modified = !modified;
}
» h1.addEventListener('click',doit);
» h1.removeEventListener('click',doit);
Conditional Operators
JavaScript has a conditional operator (a.k.a. ternary operator), which
combines a test with a choice of values. For example:
» var a = 3, b = 4;
» a == b ? 'yes' : 'no';
← "no"
The conditional operator has three parts. The first part is the test.
You can use any expression you might have used in an if() or while()
statement. The second part is the value if the test is true or truthy. The
third part is the value if not.
We can rewrite the previous code using the conditional operator:
» var h1 = document.querySelector('h1');
» var modified = false;
» function doit(event) {
event.target.style['color'] =
!modified ? 'yellow' : null;
226
Chapter 7 Interacting with CSS and Event Listeners
modified = !modified;
}
» h1.addEventListener('click',doit);
» h1.removeEventListener('click',doit);
Static Variables
At some point, you’ll probably hear that JavaScript doesn’t support static
variables, and technically, that’s correct.
In languages that do support static variables, it means that it’s possible
for a function variable to retain its value between calls. For example, you
might have something like this:
» test();
» test();
» test();
227
Chapter 7 Interacting with CSS and Event Listeners
To make the previous sample work, we change the value variable into
a .value property of the function:
» function test() {
test.value ??= 0; // undefined → 0
test.value ++;
console.log(test.value);
}
» test();
» test();
» test();
You need the identity operator (===) to avoid confusing undefined with
other falsy values. However, it’s much simpler, and more readable when
you get used to it, to use the shorter expression shown previously.
In JavaScript, this is the nullish coalescing operator that tests the first
value specifically for null or undefined, which, among other things, avoids
the confusion. You can test it like this:
» undefined ?? 23
← 23
228
Chapter 7 Interacting with CSS and Event Listeners
We can use the same technique to dispense with the state variable in
the event listener:
» var h1 = document.querySelector('h1');
» // var modified = false; // not needed
» function doit(event) {
doit.modified ??= false;
event.target.style['color'] =
!doit.modified ? 'yellow' : null;
doit.modified = !doit.modified;
}
» h1.addEventListener('click',doit);
» h1.removeEventListener('click',doit);
» var h1 = document.querySelector('h1');
» var colours
= ['red','orange','yellow','green','blue','violet'];
» function doit(event) {
// random index
var i = Math.floor(Math.random() * colours.length);
event.target.style['background-color'] = colours[i];
}
» h1.addEventListener('click',doit);
229
Chapter 7 Interacting with CSS and Event Listeners
» h1.removeEventListener('click',doit);
If you don’t like the color you ended with, you can also remove it:
» h1.style['background-color'] = null;
» var h1 = document.querySelector('h1');
» var styles = getComputedStyle(h1);
» console.log(styles['background-color']);
230
Chapter 7 Interacting with CSS and Event Listeners
» var h1 = document.querySelector('h1');
» var styles = getComputedStyle(h1);
» var originalColour = styles['background-color'];
» function doit(event) {
doit.modified ??= false;
event.target.style['background-color'] =
!doit.modified ? 'yellow' : originalColour;
doit.modified = !doit.modified;
}
» h1.addEventListener('click',doit);
Generally, it’s always a good idea to take note of the original CSS
properties if you’re going to make changes to them in JavaScript.
Don’t forget to remove the event listener, as well as the color, for future
experiments. Alternatively, you can just reload the page.
» h1.removeEventListener('click',doit);
» h1.style['background-color'] = null;
231
Chapter 7 Interacting with CSS and Event Listeners
h1.different-color {
color: #dac1a4;
}
h1.different-background {
background-color: #587272;
}
You’ll notice that there are two different classes, each with their own
property. If you wanted either class to be applied, you could add the
following in HTML:
» var h1 = document.querySelector('h1');
» h1.className = 'different-color';
If you want to apply multiple classes, you can just list them:
» var h1 = document.querySelector('h1');
» h1.className = 'different-color different-background';
232
Chapter 7 Interacting with CSS and Event Listeners
h1.className = '';
Managing more than one class in a string like this can be tricky if
you want to work with them individually. Modern JavaScript now has an
alternative way of manipulating classes using .classList:
» var h1 = document.querySelector('h1');
» h1.classList.add('different-color','different-background');
You can add as many classes as you like this way, but it’s usually used
with a single class.
You then remove the class to revert its style:
» var h1 = document.querySelector('h1');
» h1.classList.remove('different-color');
233
Chapter 7 Interacting with CSS and Event Listeners
» var h1 = document.querySelector('h1');
» function toggle(event) {
event.target.classList.toggle('different-color');
}
» h1.addEventListener('click',toggle);
» h1.removeEventListener('click',toggle);
» var h1 = document.querySelector('h1');
» h1.onclick = function(event) {
event.target.classList.toggle('different-color');
};
234
Chapter 7 Interacting with CSS and Event Listeners
» var h1 = document.querySelector('h1');
» var h1 = document.querySelector('h1');
» h1.addEventListener('click', function(event) {
event.target.classList.toggle('different-color');
});
» var h1 = document.querySelector('h1');
235
Chapter 7 Interacting with CSS and Event Listeners
This is slightly harder to read, but it allows the flexibility you get with
the .addEventListener() method.
The real problem is that the event listener is now anonymous, and you
can’t easily remove it, since you don’t have a direct reference to it. There
are two workarounds.
First, you can name the anonymous function as a property of the
element:
» var h1 = document.querySelector('h1');
» h1.removeEventListener('click', h1.fn);
» h1.outerHTML = h1.outerHTML;
236
Chapter 7 Interacting with CSS and Event Listeners
You can use the outerHTML method if you’ve started to lose track
of your added event listeners and you really don’t want to reload
your page.
For simple CSS transitions, you can use straight CSS, but for more
complex behavior, you can use JavaScript.
button#sample-button {
...
background-color: rgb(0,34,34);
237
Chapter 7 Interacting with CSS and Event Listeners
color: white;
transition: color 0.5s, background-color 0.5s
}
button#sample-button:hover {
color: rgb(0,34,34);
background-color: white;
}
Transitioning in JavaScript
In the Old Days, all transitions and animations were implemented in
JavaScript. That’s because CSS didn’t know how. Whole libraries were built
around transitions and animations, and some are used even today. Most of
the time, we don’t need them anymore.
If you really want to implement your own JavaScript transition, you
can use a modified version of the code used in the slide show project. A
transition is simply a fast repetition of changes.
Here is how you might use JavaScript to fade out the image element in
20 steps:
238
Chapter 7 Interacting with CSS and Event Listeners
}
» var timer = setInterval(fading, 100);
If you want to take this seriously, you can wrap this in a function:
Here, the image is now passed as a parameter, and the total time (2000
milliseconds) and the number of steps are also passed; inside the function,
they are used to calculate the interval for the setInterval() function and
the increment for the opacity.
However, we really don’t want to do this, especially where other more
complex transitions are involved. Thankfully, CSS comes to the rescue.
» var h1 = document.querySelector('h1');
» // Initial Properties
» h1.style.transition = 'border-color 3s';
» h1.style['border'] = 'thick solid';
239
Chapter 7 Interacting with CSS and Event Listeners
» h1.style['border-color'] = 'red';
» function doTransition(event) {
doTransition.modified ??= false;
event.target.style['border-color'] =
!doTransition.modified ? 'yellow' : 'red';
doTransition.modified = ! doTransition.modified;
}
» h1.onclick = doTransition;
When you click on the heading, the border color will eventually change
to yellow, but it will take three seconds to do so. Clicking on it again will
change it back to red.
The transition effect requires two things:
» var h1 = document.querySelector('h1');
The problem is that the browser won’t wait around for the three
seconds to complete the color change before it changes it again. Instead, it
will take a shortcut and just assign the second value.
240
Chapter 7 Interacting with CSS and Event Listeners
If the element was a different color to begin with, you will only see a
transition to the second color. If the element was already the second color,
you won’t see anything at all.
JavaScript doesn’t like to be kept waiting, but you can get it to come
back to a task when the time comes. In this case, you can assign a function
to run when the transition has ended. This requires responding to the
transitionend event:
» var h1 = document.querySelector('h1');
These examples have all used color as an obvious property to test with.
You can also use this technique with opacity:
» h1.style.transition='opacity 4s';
» h1.ontransitionend = event => {
event.target.style['opacity'] = 1;
}
» h1.style.opacity = 0;
» h1.style['overflow']='hidden';
» h1.style.transition='max-height 4s';
» h1.style['max-height'] = '2em';
» h1.ontransitionend = event => {
event.target.style['max-height'] = '2em';
241
Chapter 7 Interacting with CSS and Event Listeners
}
» h1.offsetHeight;
» h1.style['max-height'] = '0';
That last example needed some additional style changes to make the
transition more obvious. The overflow property controls what happens if
the content doesn’t fit in the box. In this case, the content is hidden instead
of overflowing.
It also needed a few tricks to make it work:
242
Chapter 7 Interacting with CSS and Event Listeners
<head>
<style>
...
</style>
...
<link rel="stylesheet" href="...">
...
</head>
The style element would contain additional styles for this page only.
Note that in this example, it is placed before the link element, which loads
an external style sheet. It could have been placed after, but being placed
before allows the external style sheet to override the internal styles.
It is generally a bad idea to define styles this way in the HTML because
it is harder to maintain, but we will use a variation of this technique in
JavaScript.
To create a style sheet in JavaScript, we use document.
createElement(), which can be used to create any HTML element at all:
243
Chapter 7 Interacting with CSS and Event Listeners
The variable style is used to add the element to the document and
will also be used later to add CSS rules.
To add the style element to the head element, the easiest method is to
use insertAdjacentElement, which allows you to add the new element
inside or outside, at the beginning or end, of the original element.
Note that the new style sheet is added at the beginning of the head
element, before any other style sheets that have already been added. This
allows the linked style sheets to override properties if desired.
» style.sheet.insertRule(`h2 {
border: medium solid white;
border-style: none none solid none;
padding-bottom: 0.5em;
}`);
CSS can be written as a single line, but you normally have line breaks
between the properties to make the code more manageable. That’s why we
used the template string literal to allow line breaks in the string.
In this example, we added some CSS that could readily have been
included in one of the standard style sheets, so it’s a bit wasted here.
However, in Chapter 9, on the Lightbox, we’ll be using this technique to
add some essential CSS that is required to make the effect work. By putting
the new style sheet at the beginning, you still have the option of overriding
some of the properties with regular style sheets.
244
Chapter 7 Interacting with CSS and Event Listeners
Summary
In this chapter, we explored working with CSS styles and JavaScript event
listeners.
CSS
CSS is the standard way of adding styling to a web page. It can be added
to HTML as a style block but is more usually added as a link to a separate
style sheet. It is also possible to add CSS styles directly to individual
elements using a style attribute.
You can make CSS style changes to elements using JavaScript, either
by adding the styles directly or by changing the element classes associated
with styles.
JavaScript can also selectively disable or enable individual style sheets.
Event Listeners
An event listener is a function set up to respond to a user event, such as
using the mouse or keyboard.
You can add event listeners to elements either through event listener
properties such as .onclick or with the .addEventListener() method.
The addEventListener() method is more flexible and allows multiple
event listeners to be added to a single element.
Event listeners can also be removed.
An event listener does not need to be a full function. It can be a
function expression, which is a function that is not stored separately. It
can also be an arrow function expression, which is a simplified version.
However, using a function expression makes it trickier to remove.
245
Chapter 7 Interacting with CSS and Event Listeners
Toggling Properties
One common application of JavaScript is to change properties back and
forth – to toggle them.
It can be useful to store the current state in a variable. This can be done
in a variable outside the function, or as a property of the function object,
acting as a kind of static variable.
246
Chapter 7 Interacting with CSS and Event Listeners
Transitions
A transition is a gradual change in the CSS style. In the past, this would
have been accomplished in JavaScript using repeated calls to a function,
in a similar way to the JavaScript slide show code. Modern CSS can handle
these transitions without extra JavaScript.
You can trigger a CSS transition by setting the transition property in
JavaScript and changing the affected CSS property. Alternatively, you can
trigger a transition by changing the class using a predefined transition.
Coming Up
The next two projects will put all of this into practice. Chapter 8 focuses
on how JavaScript can toggle the visibility of certain elements by toggling
classes.
Chapter 9 focuses on creating new elements, adding CSS styles,
hijacking HTML anchors, and more on event listeners, including working
with the keyboard.
247
CHAPTER 8
The content sections and the lists are closed. There rest of the content
is there, but it’s hidden in CSS. We’ll write code to change that.
For good measure, there’s also a section at the end that discusses
changing the behavior of radio buttons based on a technique we’ll develop
in the first exercise on content headings.
250
Chapter 8 Showing and Hiding Content
init();
function init() {
doHeadings();
doMenu();
doForm();
}
function doHeadings() {
function doMenu() {
function doForm() {
The init() function will call three functions, for the separate parts of
this project.
251
Chapter 8 Showing and Hiding Content
<div id="headings">
<h2> ... </h2>
<div>
<!-- content -->
</div>
<h2> ... </h2>
<div>
<!-- content -->
</div>
</div>
The whole of the content is inside a div element. Often divs are used
to control layout, but here the div is used as a main container.
Since a div has no specific default behavior, other than as a block
element, it is normal to give it an id or a class to identify it. In this case,
the id is headings, and the container is identified as div#headings. This is
used in both CSS and JavaScript.
There are more div elements, but they are easily identified as being
inside the main div. The same also goes for the h2 elements, which
normally define headings but will also be used to show and hide the
content.
The purpose of the inner div elements is to contain the content to be
shown or hidden after the heading. In CSS, they are identified as h2+div,
which means a div after an h2.
252
Chapter 8 Showing and Hiding Content
Using this one id, we can identify the main container and its contents
using the following selectors:
Elements Selector
Note that we’ve tried to minimize the requirements on the HTML. The
container must have an id, and the content must follow a pattern.
That’s all.
function doHeadings() {
let headings = document.querySelectorAll('div#headings>h2');
if(!headings.length) return;
}
253
Chapter 8 Showing and Hiding Content
function doHeadings(container) {
let headings = document.querySelectorAll(`${container}>h2`);
if(!headings.length) return;
}
We’ll need to supply the container in the function call in the init()
function:
function init() {
doHeadings('div#headings');
doMenu();
doForm();
}
We’ll look out for any other opportunities to generalize the code.
254
Chapter 8 Showing and Hiding Content
headings.forEach(doSomething);
where item is each item in the collection, index is the number of each
item, and collection is the whole collection for good measure. Most of
the time we’re only interested in the item.
The function doesn’t have to be a permanent named function if you’re
only using it in the forEach() method. A function expression will do:
});
});
In our case, we will need to refer to each item of the collection (each
heading), but not the rest of the parameters, so we can use a shorter syntax
without the parentheses:
headings.forEach(item => {
});
function doHeadings() {
let headings =
document.querySelectorAll('div#headings>h2');
if(!headings.length) return;
255
Chapter 8 Showing and Hiding Content
headings.forEach(heading => {
});
}
function doHeadings(container) {
...
headings.forEach(heading => {
heading.onclick = toggle;
});
function toggle() {
}
}
Note that the function is defined after you assign it, which seems
backward. You can define the function before, of course, but this way may
be easier to follow.
JavaScript doesn’t normally run out of sequence. However, the
JavaScript interpreter runs in more than one stage, and you’ll find that it
processes all var and function statements before it actually runs the code.
We say that var and function statements are hoisted (moved up).
The newer let and const statements are not hoisted.
256
Chapter 8 Showing and Hiding Content
div#headings>h2+div {
display: none;
}
div#headings>h2.open+div {
display: block;
}
div#headings>h2+div {
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 250ms, opacity 1500ms;
}
div#headings>h2.open+div {
max-height: 30em;
opacity: 1;
}
257
Chapter 8 Showing and Hiding Content
Instead of abruptly hiding and showing the div with the display
property, the max-height of the div is first set to 0. To avoid content spilling
over, the overflow property is also set to hidden.
While we’re at it, we set its opacity to 0 to make it invisible. This will
allow us to fade it in later.
To show the div, we set its height to something large enough and set its
opacity to 1. What makes the change more interesting is the transition
property that says which properties should take their time and how long.
The hardest part is working out a reasonable value for the max-height
property. If you set it too high, you’ll be waiting too long before you see the
content closing in. If you set it too low, it won’t fit the content.
You will appreciate how much is actually being done in a few lines of
CSS. All that is required of JavaScript is to toggle the class.
function toggle(event) {
event.target.classList.toggle('open');
}
258
Chapter 8 Showing and Hiding Content
function doHeadings() {
...
function toggle(event) {
// event.target.classList.toggle('open');
}
}
The opened variable will be used to track the h2 element that currently
has the class open. To begin with, no section is open, so the variable opened
is initialized with null.
Note that the original statement inside the toggle function has been
commented out. What follows will be a replacement for that code. You can,
of course, delete that statement, but it’s kept here for comparison.
Note also how the new variable relates to the inner and outer
functions. The opened variable is defined inside the doHeadings function,
so it is local to doHeadings. However, it is defined outside the outer toggle
function. There is no official term for this, but we can informally refer to
this as relatively global.
259
Chapter 8 Showing and Hiding Content
The first job of the toggle function should be to hide the previous
section; that is to remove the open class from the currently selected h2:
function toggle(event) {
// event.target.classList.toggle('open');
opened.classList.remove('open'); // not yet
}
The classList.remove function will look for the open class in the
element and remove it if it’s there; it has no effect if it isn’t.
If you try this, you’ll get an error to begin with. That’s because no
section is open, and opened is set to null; you can’t call methods on null.
This means we’ll have to test it first:
function toggle(event) {
// event.target.classList.toggle('open');
if(opened) opened.classList.remove('open');
}
Remember that if( ... ) will test for true or false, but also for
something or nothing. In this case, null counts as nothing; if opened is
something, we can remove the class.
The next step will be to open the selected section by adding open to
the selected h2’s class list. We will also assign that h2 as the new opened
variable:
260
Chapter 8 Showing and Hiding Content
function toggle(event) {
// event.target.classList.toggle('open');
if(opened) opened.classList.remove('open'); // close
event.target.classList.add('open');
opened = event.target;
}
If you run this now, you will see that only one section is opened
at a time.
function toggle(event) {
// event.target.classList.toggle('open');
261
Chapter 8 Showing and Hiding Content
/* toggleHeadingContent(container)
================================================
Toggles heading sections. Use the following structure:
div#containerid
h2
div
content
h2
div
content
h2
div
content
================================================ */
function toggleHeadingContent(container) {
...
}
262
Chapter 8 Showing and Hiding Content
You can be as detailed as you like, but try not to get carried away.
Finally, of course, you’ll need to change the name in the init()
function code:
function init() {
toggleHeadingContent('div#headings');
doMenu();
doForm();
}
Toggling Lists
An HTML list is a structure that contains one or more list items. In its
simplest form, the HTML looks like this:
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</ul>
• Apple
• Banana
• Cherry
263
Chapter 8 Showing and Hiding Content
Using CSS, you can make many changes to the appearance of the list,
including the numbering style.
You can also have nested lists, lists within lists:
<ul>
<li>Fruit
<ul>
<li>apple</li>
<li>banana</li>
<li>cherry</li>
</ul>
</li>
<li>Instruments
<ul>
<li>accordion</li>
<li>banjo</li>
<li>cor anglais</li>
</ul>
</li>
</ul>
• apple
• banana
• cherry
• Instruments
• accordion
• banjo
• cor anglais
264
Chapter 8 Showing and Hiding Content
Here, the main list items have some text followed by another ul; the
text acts as a heading for the nested list.
Lists are often used as a menu, ranging from simple navigation buttons
to menus with drop-down submenus, all through the magic of CSS.
If the list is large enough, it starts to dominate the page, and it becomes
convenient to find a way of opening and closing sections of the list. This is
what we’re working on in this part of the project.
Lists can, of course, be used for many things, but one common use
is as lists of links to act as a menu. In the past, other structures, such
as tables, were also used for menus due to the difficulty of making
them look right, especially if the menu was supposed to be horizontal.
Thankfully, those days are over, and CSS can make list menus look
just right. The hardest part now is deciding which of the many ways
you want to style your lists.
<ul id="menu>
</ul>
265
Chapter 8 Showing and Hiding Content
function init() {
toggleHeadingContent('div#headings');
doMenu('menu');
}
function doMenu(menuId) {
function doMenu(menuId) {
let ul = document.querySelector(`ul#${menuId}`);
}
function doMenu() {
let ul = document.querySelector('ul#menu');
ul.onclick = toggle;
function toggle(event) {
}
}
266
Chapter 8 Showing and Hiding Content
267
Chapter 8 Showing and Hiding Content
function toggle(event) {
if(!event.target.querySelector('ul')) return;
}
function toggle(event) {
if(!event.target.querySelector('ul')) return;
event.target.classList.toggle('open');
}
li:has(ul) {
...
}
268
Chapter 8 Showing and Hiding Content
which means any li that contains a ul. If you look at the style sheet
itself, it’s not quite so simple, since it still has to cater for nonsupporting
browsers:
li:is(.nested,:has(ul)) {
...
}
There’s more to the selectors since they include references to the open
class, the ul#menu main list, and other content.
The point is nonsupporting browsers will need some help in the form
of a class, called nested.
To get this working cross-platform, then, we’ll need to add the nested
class to the items with nested lists on nonsupporting browsers. We don’t
want to do this manually, so we’ll get JavaScript to do the hard work.
In the past, there was an awful lot of “browser sniffing” – checking
which browser was being used. That’s definitely a bad idea, since it was
impossible to keep up with the various browsers and versions. A far better
idea is testing for the feature itself.
269
Chapter 8 Showing and Hiding Content
You should only consider this sort of thing if the feature has been
standardized but not yet completely supported across the board.
Relying on nonstandard features will guarantee heartache in
the future.
CSS.supports('selector(:has(ul))')
function doMenu() {
let ul=document.querySelector('ul#menu');
ul.onclick=toggle;
if(!CSS.supports('selector(:has(ul))')) {
ul.querySelectorAll('li').forEach(li => {
270
Chapter 8 Showing and Hiding Content
});
}
...
}
function doMenu() {
let ul=document.querySelector('ul#menu');
ul.onclick=toggle;
if(!CSS.supports('selector(:has(ul))')) {
ul.querySelectorAll('li').forEach(li => {
if(li.querySelector('ul')) li.classList.
add('nested');
});
}
...
}
271
Chapter 8 Showing and Hiding Content
function doMenu() {
let ul=document.querySelector('ul#menu');
ul.onclick=toggle;
// if(!CSS.supports('selector(:has(ul))')) {
ul.querySelectorAll('li').forEach(li => {
if(li.querySelector('ul')) li.classList.
add('nested');
});
// }
...
}
if(!event.target.querySelector('ul')) return;
272
Chapter 8 Showing and Hiding Content
function toggle(event) {
if(!event.target.matches(':is(.nested,:has(ul))')) return;
event.target.classList.toggle('open');
}
» var h1 = document.querySelector('h1');
» h1.matches('body *');
← true
This tells us that the h1 element is inside the body element, which, of
course, it is.
The :is(.nested,:has(ul)) selector succeeds if either the .nested
selector or :has(ul) selector matches. If not, we return from the function.
Testing for properties is more efficient than searching inside an
element for more elements.
/* toggleNestedLists(menuId)
================================================
Toggles nested lists.
<ul id="...">
273
Chapter 8 Showing and Hiding Content
<li>item
<ul>
<li>item</li>
<li>item</li>
</ul>
</li>
<li>item
<ul>
<li>item</li>
<li>item</li>
</ul>
</li>
</ul>
================================================ */
function toggleNestedLists(menuId) {
...
}
function init() {
toggleHeadingContent('div#headings');
toggleNestedLists('menu');
}
274
Chapter 8 Showing and Hiding Content
<form id="music">
<p>Choose a musical style</p>
<label><input type="radio" name="musical-style"
value="jazz">Jazz</label>
<label><input type="radio" name="musical-style"
value="classical">Classical</label>
<label><input type="radio" name="musical-style"
value="newage">New Age</label>
<label><input type="radio" name="musical-style"
value="rocknroll">Rock & Roll</label>
<output name="selected">
<button name="ok">OK</button>
</form>
275
Chapter 8 Showing and Hiding Content
• They all have the same name attribute, which is how the
browser knows they’re part of the same group.
function doForm() {
let form = document.querySelector('form#music');
function doForm() {
let form = document.querySelector('form#music');
let buttons = form.elements['musical-style'];
...
}
Normally, when you fetch from the elements collection, you get
a single form element. In the case of radio buttons, you get another
collection; more specifically it’s a RadioNodeList, but we’re only interested
in the fact that we can iterate through the collection.
Remember in the contents headings, we wanted to select one element
only. That behavior is built in to radio buttons. However, we also wanted
to be able to deselect a heading, and that certainly isn’t built in to radio
buttons. We’ll use the same technique of using a tracking variable.
276
Chapter 8 Showing and Hiding Content
function doForm() {
let form = document.querySelector('form#music');
let buttons = form.elements['musical-style'];
let selected = null;
...
}
function doForm() {
let form = document.querySelector('form#music');
let buttons = form.elements['musical-style'];
let selected = null;
buttons.forEach(b => {
b.onclick = event => {
};
});
...
}
We’ve got two arrow function expressions here: one to iterate through
the buttons collection and one for the event listener.
The event listener will check whether the clicked button is the same as
last time. If it is, we’ll deselect the button by setting its .checked property
to false and clearing out the selected variable.
If it’s not the same, we’ll just remember that in the selected variable.
The first time, of course, there was no last time, so the answer will
be false.
277
Chapter 8 Showing and Hiding Content
buttons.forEach(b => {
b.onclick = event => {
if(event.target == selected) {
event.target.checked = false;
selected = null;
}
else selected = event.target;
};
});
function doForm() {
function deselectableRadio(buttons) {
buttons.forEach(b => {
b.onclick = event => {
if(event.target == selected) {
event.target.checked = false;
selected = null;
}
else selected = event.target;
};
});
}
278
Chapter 8 Showing and Hiding Content
function deselectableRadio(buttons) {
deselectableRadio.selected ??= null;
buttons.forEach(b => {
b.onclick = event => {
if(event.target == deselectableRadio.selected) {
event.target.checked = false;
deselectableRadio.selected = null;
}
else deselectableRadio.selected = event.target;
};
});
}
We should now be able to test the function. Comment out the old code,
just in case, and include a call to the new function, passing the buttons
collection:
function doForm() {
let form = document.querySelector('form#music');
let buttons = form.elements['musical-style'];
deselectableRadio(buttons);
/*
...
*/
form.elements['ok'].onclick = event => {
event.preventDefault();
};
}
279
Chapter 8 Showing and Hiding Content
function deselectableRadio(buttons) {
...
}
If it works, you can delete the commented code and move the function
to library.js as you did with the other functions in this chapter. You can
include a comment block like this:
/* deselectableRadio(buttons)
================================================
Allows a group of radio buttons to be deselected.
================================================ */
Of course, you may not want your radio buttons to be deselectable, but
it’s nice to have a choice.
Summary
In this project, we used JavaScript to show and hide content with user
interaction. We used many of the concepts developed in Chapter 7.
You can show and hide content in CSS. One simple method is to
change the display property between block and none, which is the
method used in the nested list. A more complex method, which allows
transition animation effects, is to change the max-height and opacity
properties.
The main thrust of the code is to use the click event to toggle the class
of the element. The CSS handles what happens to the content that is either
nested or adjacent to the affected element.
In the headings exercise, there are multiple elements that need
to be attached to the event listener. These are fetched using the
.querySelectorAll() function. We can then iterate through the collection
with the .forEach() method.
280
Chapter 8 Showing and Hiding Content
Coming Up
The content we’ve been working with is already there in the page – it’s
just being hidden. In Chapter 10, we’ll look at how to load new content
on demand.
Before that, however, we’re going to work on a lightbox project. This
is where images pop up on the screen on demand. To do this, we’ll work
through adding HTML content, adding CSS, and triggering activity with
the mouse and the keyboard.
In Chapter 10, we’ll also work on loading image content and data from
the server using Ajax.
281
CHAPTER 9
Project: Building
a Lightbox Gallery
Many modern image gallery pages include a pop-up larger image. This has
come to be known as lightbox after an original script by Lokesh Dhakar
(see https://en.wikipedia.org/wiki/Lightbox_(JavaScript)).
You will have seen something similar on many websites. When you
click on an image thumbnail, a larger one appears over the rest of the page.
A typical lightbox effect looks like Figure 9-1.
284
Chapter 9 Project: Building a Lightbox Gallery
<div id="catalogue">
<a href="..."><img src="..." title="..."></a>
<a href="..."><img src="..." title="..."></a>
<a href="..."><img src="..." title="..."></a>
<!-- etc -->
<div>
285
Chapter 9 Project: Building a Lightbox Gallery
In this case, the container is a div element, which is the most generic
block element. We can use the id catalogue to identify the container to be
processed. The id is also used in CSS to give the container and contents
any special appearance you like.
The images will be ordinary img elements showing thumbnail
versions in the src attribute. We have omitted other attributes here, but
the title attribute is useful. Normally, the title appears as tooltip text
when you hover the mouse over it, but we will use it as caption text for the
larger image.
Each image is wrapped inside an anchor (a) element linking to a larger
version of the image. By itself, the anchor would normally reload the page
with the linked image, but we’ll use it to display the larger image in a
popup instead.
The JavaScript starts in the usual way, but we have already included
a call to the new function complete with a parameter for the container of
the images:
286
Chapter 9 Project: Building a Lightbox Gallery
init();
function init() {
doLightbox('div#catalogue');
}
function doLightbox(container) {
}
If you load the page now and click on one of the images, all you’ll see
is that the page is reloaded with the larger image. That’s because the larger
image is referenced in the href attribute of the surrounding anchor.
You’ll also see that the color of each of the thumbnails is desaturated
(mostly grayscale) when the mouse isn’t over it. That’s all done in CSS.
Inside the doLightbox() function, we can use the selector string with
querySelectorAll() to find the anchors containing the images:
function doLightbox(container) {
let anchors = document.querySelectorAll(`${container}>a`);
}
which means all of the anchors that are children of the div element.
Once we have these anchors, we can change their behavior to load the
larger image into a popup.
287
Chapter 9 Project: Building a Lightbox Gallery
function doLightbox(container) {
let anchors = document.querySelectorAll(`${container}>a`);
anchors.forEach(a => {
a.onclick = show;
});
}
}
288
Chapter 9 Project: Building a Lightbox Gallery
function show(event) {
event.preventDefault();
}
289
Chapter 9 Project: Building a Lightbox Gallery
290
Chapter 9 Project: Building a Lightbox Gallery
The figure, together with img and figcaption elements, will sit in
front of the background div.
function doLightbox(container) {
let anchors = document.querySelectorAll(`${conainer}>a`);
anchors.forEach(a => {
a.onclick = show;
});
// Create Elements
let background = document.createElement('div');
...
}
This should be enough for JavaScript to work with, but there’s going to
be a little CSS involved. To make sure that CSS doesn’t get things mixed up,
we’ll add ids to the background and figure elements. The CSS will know
how to handle the nested img and figcaption elements.
function (container) {
...
// Create Elements
let background = document.createElement('div');
291
Chapter 9 Project: Building a Lightbox Gallery
background.id = 'lightbox-background';
figure.id = 'lightbox';
...
}
The background div will stand alone, but the image and caption
should be contained inside the container element. This can be done using
appendChild():
function doLightbox(container) {
...
// Create Elements
...
background.id = 'lightbox-background';
figure.id = 'lightbox';
figure.appendChild(img);
figure.appendChild(figcaption);
...
}
292
Chapter 9 Project: Building a Lightbox Gallery
function (container) {
...
// Create Elements
...
figure.appendChild(img);
figure.appendChild(figcaption);
document.body.appendChild(background);
document.body.appendChild(figure);
...
}
We now have some empty elements added to the end of the document
body. If you reload the page, you won’t see very much: all of the new
elements are at the end of the body element, but they are empty, so they
take up no space.
The next step will begin to define their appearance.
293
Chapter 9 Project: Building a Lightbox Gallery
To add the CSS style, you first create a style element and add it to the
head of the document:
function doLightbox(container) {
...
// Create Elements
...
// Style Sheet
let style = document.createElement('style');
document.head.insertAdjacentElement('afterbegin',
style);
...
}
Note that the new style element is added to the beginning of the head
element. This allows additional real style sheets to be added after, meaning
that they can override some of your own styles.
/* Background Div: */
div#lightbox-background {
position: fixed;
top: 0; left: 0; width:100%; height: 100%;
z-index: 1;
background-color: rgb(0,0,0,0.5);
}
294
Chapter 9 Project: Building a Lightbox Gallery
/* Container Div: */
figure#lightbox {
position: fixed;
top: 50%; left: 50%; margin-right: -50%;
transform: translate(-50%, -50%);
z-index: 2;
}
The actual color and opacity of the background are arbitrary, and you
can easily override these values in an additional real style sheet.
295
Chapter 9 Project: Building a Lightbox Gallery
The two sections in the CSS shown previously are called rules and can
be added using insertRule():
function doLightbox(container) {
...
// Style Sheet
let style = document.createElement('style');
document.head.insertAdjacentElement('afterbegin',
style);
...
}
296
Chapter 9 Project: Building a Lightbox Gallery
We’ve used template literals to allow line breaks and indentation also
for readability. Remember, CSS can have as much additional spacing as
you like.
As you see, CSS has a tendency to get verbose, so it will never be very
compact. Fortunately, that’s enough CSS to achieve the effect.
What you’ll see now is the whole page covered by the translucent
background. The figure appears as a small box but is otherwise invisible
because it has no content yet.
function doLightbox(container) {
...
function show(event) {
event.preventDefault();
}
function hide(event) {
}
}
297
Chapter 9 Project: Building a Lightbox Gallery
To hide an element, you can set its display property to none. To show
the element, set its display property to block, how we normally want
them displayed:
function doLightbox(container) {
...
function show(event) {
event.preventDefault();
background.style.display
= figure.style.display
= 'block';
}
function hide(event) {
background.style.display
= figure.style.display
= 'none';
}
}
function doLightbox(container) {
...
298
Chapter 9 Project: Building a Lightbox Gallery
function show(event) {
...
}
...
}
This effectively turns the background into one giant button. You can
click on the background now to hide it, but you should also call the hide()
function when the script is first loaded:
function doLightbox(container) {
...
...
}
At this stage, you can test showing and hiding by clicking on an image
and the background, but all you’ll see is the background showing and
hiding. Next, you’ll need to populate the container with the image and its
caption.
299
Chapter 9 Project: Building a Lightbox Gallery
<a href="...">
<img src="..." title="...">
</a>
The anchor itself is just a wrapper, and most of the visual space will be
taken by the image. Therefore, when you click on this, you will actually be
clicking on the image.
The image doesn’t have an event listener and has no default clicking
behavior, so it has no interest in the click. That click event is passed on
to its container, the anchor element. As we saw in Chapter 7 on event
listeners, the event has bubbled.
We can prevent bubbling from happening, but in this case, that’s
exactly what we want. The containing anchor does have an event listener,
and it is this function that will do the job.
In summary, you click on the image, but the anchor will process
the click.
Inside the event listener, there are two important objects:
300
Chapter 9 Project: Building a Lightbox Gallery
function show(event) {
event.preventDefault();
As for the caption, that will come from the original image’s title
attribute. That means copying the title from the event.target to the
caption’s textContent:
function show(event) {
event.preventDefault();
background.style.display = container.style.display =
'block';
}
The pop-up image element is currently devoid of the alt and title
attributes. These properties are always a good idea for images. We can
copy them at the same time:
function show(event) {
event.preventDefault();
301
Chapter 9 Project: Building a Lightbox Gallery
background.style.display=container.style.display='block';
}
You won’t see the image’s alt or title attributes, but the alt is
required in case the image can’t be seen, and the title will typically
appear if you hover the mouse over the image.
If you load the page now, it will work, but it will show and hide very
abruptly. Later, we’ll make that more interesting.
First, however, we’ll need to solve a little problem of timing.
Timing Problems
If you test the code on your own machine, you may find that things work
mostly as intended. However, if you run the code from a real web server,
there may be problems due to a delay in loading the image.
For example, if you change the href in the first anchor to
<a href="http://pure-javascript.net/images/random.jpg">...</a>
there will be a short delay when you click on it. However, this delay will be
long enough that the image won’t appear in the popup.
The problem is inherent in the way JavaScript runs. Here is a simple
script to illustrate the problem:
302
Chapter 9 Project: Building a Lightbox Gallery
The image variable is a virtual image for testing purposes. You won’t
actually see the image. Adding the random query string after random.
jpg ensures that the browser doesn’t cache the image, since that would
interfere with the experiment.
The statement image.src = ...; causes the browser to request an
image from the server. However, that could take some time, and JavaScript
doesn’t like to hang around waiting. As a result, the next line console.
log(...) will run before the new image has arrived, so image.width won’t
be ready. You will probably get 0, or possibly a previous value.
We say that the image is loaded asynchronously. That’s also going
to be a problem later when working with Ajax, which also involves
communicating with the web server.
Modern JavaScript has new methods for dealing with asynchronous
code, but the delay with images is easily handled with something more
traditional. An image can have a load event listener, which will be
triggered when the image has fully arrived. We can use that to delay
running part of the code:
» function doit(event) {
console.log(image.width);
}
» image.onload = doit;
» image.src =
`https://pure-javascript.net/images/random.jpg?${Math.
random()}`;
» console.log('Meanwhile ...');
303
Chapter 9 Project: Building a Lightbox Gallery
Notice that the message Meanwhile will be printed before the image
width, as it is executed without waiting.
This event listener is simple enough not to require a named function,
so we can use a function expression:
» image.onload = function(event) {
console.log(image.width);
}
Back to our lightbox project, we can use this idea to hold off displaying
the popup until the image has arrived:
function show(event) {
event.preventDefault();
// background.style.display = figure.style.
display='block';
img.onload = event => {
background.style.display = figure.style.display =
'block';
};
}
304
Chapter 9 Project: Building a Lightbox Gallery
Now we have prepared the content for displaying, but only display
once the image has arrived.
You can now test the project.
Key Behavior
Hide image
or First image
Previous imageIf at the beginning, wrap around to the last image
Next imageIf at the end, wrap around to the first image
or Last image
305
Chapter 9 Project: Building a Lightbox Gallery
So as not to interfere with normal use of the keyboard in the rest of the
page, we will assign an event listener only when the image is shown and
remove it when it is hidden. We’ll call the function doKeys().
function show(event) {
...
document.addEventListener('keydown',doKeys);
}
function hide(event) {
...
document.removeEventListener('keydown',doKeys);
}
function doKeys(event) {
function doLightbox(container) {
// --snip--
function doKeys(event) {
event.preventDefault();
}
}
306
Chapter 9 Project: Building a Lightbox Gallery
Later we will allow the arrow keys to navigate through the image
collection. However, they normally scroll the page around, so we want
to stop that while the image is showing. Adding preventDefault() will
prevent any such keystrokes from being further processed.
307
Chapter 9 Project: Building a Lightbox Gallery
else if(n==4) {
console.log('four');
}
else {
console.log('whatever');
}
The switch statement does nearly the same thing, but not quite. To
begin with, it looks like this, but it won’t work yet:
308
Chapter 9 Project: Building a Lightbox Gallery
Now you get to the next problem. If you enter, say, the number 2,
you’ll find that you get all of the output statements from two onward.
Once you enter a case block, you continue through the rest of them, even
if the following cases don’t match. This is regarded as baffling even to
experienced developers, but it’s there for historical reasons. We’ll even take
advantage of that in a moment.
Meanwhile, the solution is to add a break statement to each case once
we’ve dealt with it:
309
Chapter 9 Project: Building a Lightbox Gallery
You don’t add one after the default case because it’s the last one and
there’s nowhere else to go.
Back to the project, we’ll add the outline of the switch block to test for
the various keys:
function doKeys(event) {
event.preventDefault();
switch(event.key) {
}
}
function doKeys(event) {
event.preventDefault();
switch(event.key) {
case 'Esc': // Old Version
case 'Escape':
hide();
break;
}
}
The correct value for the Escape key is Escape. However, some older
browsers will use the older Esc name, so it’s best to check for both. This is
done by including an empty case with the alternative value and letting it
fall through to the next case.
310
Chapter 9 Project: Building a Lightbox Gallery
Note that we’re taking advantage of the fact that the code continues
through subsequent case blocks until either it encounters a break or it
gets to the end. That’s how we allow the Esc case to fall through to the
Escape case.
You can now test the page and check that the Escape key does indeed
hide the pop-up image.
311
Chapter 9 Project: Building a Lightbox Gallery
function doLightbox(container) {
let images = document.querySelectorAll(`${container}>a`);
anchors.forEach(a => {
a.onclick = show;
});
let currentAnchor; // current anchor
...
}
function doLightbox(container) {
...
function show(event) {
event.preventDefault();
currentAnchor = event.currentTarget;
...
}
...
}
312
Chapter 9 Project: Building a Lightbox Gallery
function show(event) {
event.preventDefault();
currentAnchor = event.currentTarget;
// populate image element
img.src = currentAnchor.href;
...
}
The next step is to move the loading part of the code into a separate
function. We’ll call it loadImage():
function loadImage() {
// populate image element
img.src = currentAnchor.href;
// caption text
// figcaption.textContent = img.alt = img.title
// = event.target.title;
function show(event) {
event.preventDefault();
currentAnchor = event.currentTarget;
// code moved to loadImage()
document.addEventListener('keydown',doKeys);
}
We’ve also cut the code out of the show() function and temporarily
replaced it with a comment.
313
Chapter 9 Project: Building a Lightbox Gallery
function loadImage() {
let currentImage = currentAnchor.querySelector('img');
// populate image element
img.src = currentAnchor.href;
// caption text
figcaption.textContent = img.alt = img.title
= currentImage.title;
// background.style.display = figure.style.
display='block';
img.onload = event => {
background.style.display = figure.style.display =
'block';
};
}
314
Chapter 9 Project: Building a Lightbox Gallery
function show(event) {
event.preventDefault();
currentAnchor = event.currentTarget;
loadImage();
document.addEventListener('keydown',doKeys);
}
At this point, everything should work the same as before, but now we’ll
be able to reuse the loadImage() function from a different section of the
code. That will be in response to the arrow keys.
switch(event.key) {
case 'Esc': // Old Version
case 'Escape':
hide();
break;
case 'Left': // Old Version
case 'ArrowLeft':
// previous image
break;
case 'Right': // Old Version
case 'ArrowRight':
// next image
break;
}
315
Chapter 9 Project: Building a Lightbox Gallery
In older browsers, the keys were called Left and Right but now have
the word Arrow prepended. Again we do this with empty case blocks.
To get the previous or next anchor, you will need
switch(event.key) {
case 'Esc': // Old Version
case 'Escape':
hide();
break;
case 'Left': // Old Version
case 'ArrowLeft':
currentAnchor = currentAnchor.previousElementSibling;
loadImage();
break;
case 'Right': // Old Version
case 'ArrowRight':
currentAnchor = currentAnchor.nextElementSibling;
loadImage();
break;
}
If you test it, you will find an error if you try to go too far. There is no
image before the first one and none after the last one. We’ll need to take
that into account, but first we’ll work on selecting the first and last images.
316
Chapter 9 Project: Building a Lightbox Gallery
switch(event.key) {
...
case 'Home':
case 'Up': // Old Version
case 'ArrowUp':
// first image
break;
case 'End':
case 'Down': // Old Version
case 'ArrowDown':
// last image
break;
}
Again we have the old and new versions of the arrow key names; we
also have home and end keys as synonyms for the up and down arrow keys,
respectively.
JavaScript doesn’t have a first or last sibling property, unlike adjacent
siblings, so we will need a different approach.
Although it isn’t possible to get the first or last siblings, it is possible
to get the first or last children of an element. The trick is to find the parent
of the current anchor and then get the first or last child of that parent. You
can use something like this:
317
Chapter 9 Project: Building a Lightbox Gallery
The parentNode is the element that contains the current anchor: the
container div. From there, you can select the first or last element, again
skipping the white space and comments.
We can now add the code as follows:
switch(event.key) {
...
case 'Home':
case 'Up': // Old Version
case 'ArrowUp':
currentAnchor = currentAnchor.parentNode.
firstElementChild;
loadImage();
break;
case 'End':
case 'Down': // Old Version
case 'ArrowDown':
currentAnchor = currentAnchor.parentNode.
lastElementChild;
loadImage();
break;
}
Now, back to the problem of the left and right arrow keys. It would be
reasonable to expect the collection to cycle; that is the image before the
first should be the last in the collection, and the image after the last should
be the first in the collection.
318
Chapter 9 Project: Building a Lightbox Gallery
» var a = null;
» console.log(a || 'Banana');
"Banana"
// Previous Image
currentAnchor = currentAnchor.previousElementSibling ||
currentAnchor.parentNode.lastElementChild;
// Next Image
currentAnchor = currentAnchor.nextElementSibling ||
currentAnchor.parentNode.firstElementChild;
switch(event.key) {
...
case 'Left': // Old Version
case 'ArrowLeft':
currentAnchor = currentAnchor.previousElementSibling ||
currentAnchor.parentNode.lastElementChild;
loadImage();
break;
case 'Right': // Old Version
case 'ArrowRight':
currentAnchor = currentAnchor.nextElementSibling ||
currentAnchor.parentNode.firstElementChild;
319
Chapter 9 Project: Building a Lightbox Gallery
loadImage();
break;
...
}
You can now test the lightbox gallery complete with keyboard
navigation.
figure#lightbox {
transform-origin: 0 0;
transition:
transform 1s cubic-bezier(0.175, 0.885, 0.32, 1.275),
opacity 2s;
}
figure#lightbox.closed {
opacity: 0;
transform: scale(0) translate(-50%, -50%);
}
figure#lightbox.open {
opacity: 1;
transform: scale(1) translate(-50%, -50%);
}
320
Chapter 9 Project: Building a Lightbox Gallery
When the figure is closed, its opacity is set to 0. The figure is also
scaled down to nothing. The result is that you can’t see anything
If the figure is open, the opacity and scale will change to normal
visibility and normal scale. The special effect, however, is in the
transition property.
In CSS, transition basically means change slowly. In this case, the
opacity changes over one second, to produce a fade-in effect. The scale
increases over two seconds, but not evenly, due to the cubic-bezier value.
To allow CSS to do its job, we’ll make two changes. First, we’ll only
change the display property of the background, but not the figure:
function loadImage() {
...
function show(event) {
...
}
function hide(event) {
// background.style.display = figure.style.display = 'none';
background.style.display = 'none';
...
}
321
Chapter 9 Project: Building a Lightbox Gallery
function loadImage() {
...
function show(event) {
...
}
function hide(event) {
// background.style.display = figure.style.display = 'none';
background.style.display = 'none';
figure.classList.remove('open');
figure.classList.add('closed');
...
}
Normally, you would only need a single class to toggle between two
states, as we’ve done in other projects. However, in this case, this would
have involved setting the opacity of the figure to 0, which would interfere
with the previous versions of the code.
322
Chapter 9 Project: Building a Lightbox Gallery
/* function doLightbox(container)
================================================
Function to implement lightbox functionality for
a gallery of images.
<div id="...">
<a href="..."><img src="..." title="..."></a>
<a href="..."><img src="..." title="..."></a>
<a href="..."><img src="..." title="..."></a>
</div>
323
Chapter 9 Project: Building a Lightbox Gallery
// Fading
let style = document.createElement('style');
document.head.insertAdjacentElement('afterbegin',
style);
style.sheet.insertRule(`
div#slides>img {
opacity: 0;
}
`);
style.sheet.insertRule(`
div#slides>img.showing {
transition: opacity 1s;
opacity: 1;
}
`);
function toggle() {
...
}
...
}
324
Chapter 9 Project: Building a Lightbox Gallery
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber].src}`;
img.classList.add('showing');
...
}
However, if the class has already been added, adding it again has no
effect, and the transition won’t be triggered. You’ll have to remove it first:
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber].src}`;
img.classList.remove('showing');
img.classList.add('showing');
...
}
325
Chapter 9 Project: Building a Lightbox Gallery
This still won’t work, because the browser will take a shortcut to save
unnecessary extra work. If you’re going to remove a class and add it again,
the browser decides to do nothing, so the transition still won’t be triggered.
However, if we can trick the browser into updating the document between
those two statements, it will need to remove and then add the class as
required, and so the transition will be triggered.
One way to force an update is to read the size of an element. Since size
may be affected by CSS, the browser will have to apply the change to the
class before measuring the element. Then the next change in the class will
also be applied:
function next() {
// Populate Image
img.src = `images/slides/${images[slideNumber].src}`;
img.classList.remove('showing');
img.offsetHeight;
img.classList.add('showing');
...
}
326
Chapter 9 Project: Building a Lightbox Gallery
img.classList.remove('showing');
void img.offsetHeight;
img.classList.add('showing');
The void operator will always result in undefined, which can be useful
if you don’t have any stray values lying around in your code.
Cross Fading
The only problem with the fade in previously is that every new image starts
blank: that’s the flash you see when the image changes. If you want to fade
from one image to the next, that is called cross fading.
To implement a cross fade, you’ll need an extra img element, sitting
over the original, so you can fade from one to the other. You’ll see the
process in Figures 9-3 to 9-5. Using the extra image:
328
Chapter 9 Project: Building a Lightbox Gallery
// Cross Fade
let crossFade = img.cloneNode();
crossFade.id = 'crossfade';
img.insertAdjacentElement('afterend', crossFade);
...
}
The new img element is inserted immediately after the original so that
it will be displayed after.
329
Chapter 9 Project: Building a Lightbox Gallery
style.sheet.insertRule(`
div#slidese {
position: relative;
}
`);
style.sheet.insertRule(`
div#slides>img#crossfade {
display: block;
position: absolute;
top: 0; left: 0;
opacity: 1;
}
`);
style.sheet.insertRule(`
div#slides>img#crossfade.fading {
transition: opacity 2500ms;
opacity: 0;
}
`);
Here, we’ll leave the original image alone and let the new crossFade
image do all of the hard work. Apart from the opacity and transition
properties:
330
Chapter 9 Project: Building a Lightbox Gallery
function next() {
// Copy to Cross Fade
crossFade.src = img.src;
crossFade.alt = img.alt;
...
}
function next() {
crossFade.src = img.src;
img.src = `images/slides/${images[slideNumber].src}`;
// img.classList.remove('showing');
// img.offsetHeight;
// img.classList.add('showing');
crossFade.classList.remove('fading');
crossFade.offsetHeight;
crossFade.classList.add('fading');
...
}
331
Chapter 9 Project: Building a Lightbox Gallery
Again the previous code has been commented out because you won’t
need it anymore. Normally, you would expect to delete unwanted code,
but we have left it here in the form of comments so that you can compare
the two techniques. You might even decide that you prefer it the old way.
Summary
In this project, we developed a lightbox utility to pop up larger images from
smaller thumbnails. We also applied these techniques to enhancing the
slide show from Chapter 4.
Hijacking Anchors
The thumbnails for the slide show are contained in HTML anchors that
reference the larger version. We can intercept these anchors by applying a
click event listener to the anchor and stopping the default behavior.
Both the anchor and the nested image contain useful data in addition
to the image itself: the anchor has the reference to the larger image, and
the image has a title attribute that we can use as a caption.
When you click on an image in an anchor, both the image and the
anchor experience the click event. To extract the right data from the right
element, we distinguish between target, which refers to the image clicked
on, and currentTarget, which refers to the containing anchor.
332
Chapter 9 Project: Building a Lightbox Gallery
The same can be said for additional styles for the new elements. It’s
possible to create a style block and add it to the head section of the web
page. It’s best to add it to the beginning so that external style sheets can be
applied afterward.
The new style block should contain just enough CSS for the effect
to work. This includes setting the background to cover the screen
translucently and the CSS to allow the popup to appear in the center of the
screen. An additional style sheet can handle the cosmetic aspects.
Timing
Since images are loaded as a separate resource, they are always loaded
asynchronously. This creates a problem when relying on an image that
hasn’t yet loaded.
The onload event can be used to trigger wait until an image has loaded
before attempting to use it.
333
Chapter 9 Project: Building a Lightbox Gallery
Refactoring
Refactoring refers to changing the code so that it behaves in the same way
but is more adaptable. This is particularly important when trying to reuse
code that had limited application during its development.
Cross Fade
Although implementing a fade is simple, by changing a class to trigger a
transition, creating a cross fade requires additional work.
To create a cross fade, we can clone the existing image and use CSS
to position it over the original. The original image is then copied into the
cross fade image, and the original image is replaced. The cross fade is
effected by fading out the cross fade image, allowing the new image to
show through.
334
Chapter 9 Project: Building a Lightbox Gallery
Coming Up
The lightbox utility we developed here can be used with any gallery
project. However, it does require that you have prepared a div container
with a collection of anchors and images, and so it is not quite as
convenient or as flexible as it might be.
To make using the lightbox simpler, we can instead have a CSV file
with image file names and captions. Then JavaScript can read the file and
generate the anchors and images for us. It would also be easier to make
changes without the inconvenience of toying with the HTML.
The same would apply to the slide show, where we could fetch a list of
images and use that to populate the slide show.
To fetch more data from the web server requires a technique called
Ajax. Ajax allows JavaScript to request and receive additional data. This
data may be simple text, or it may be more complex. We can then use the
data to make changes to the current page.
In the next chapter, we will look at an introduction to Ajax and how we
can use it to load additional content from the web server.
335
CHAPTER 10
Project: An
Introduction to Ajax
If you’re trying to run this project without a web server, you’re
probably out of luck. JavaScript cannot make Ajax requests to
different domains unless the other domain specifically allows this.
This is called the Same-Origin Policy.
Modern browsers tend to regard different documents opened locally
(via File: Open ...) as coming from different domains, even if they’re
in the same folder. As a result the Ajax requests will be denied. Files
fetched from the same location using the http:// protocol are
regarded as from same origin, which is why you’ll need a web server
for this chapter.
The only part of this project that will work is the last section, since
it specifically accesses a different web server.You don’t need a
heavy-duty web server to run this project. You can use a light-duty
one from https://github.com/manngo/micro-web-server/
releases/latest.
In some of the projects so far, there was a lot of static content. The
slide show uses a hard-coded list of images. The project on showing and
hiding content manipulated existing content on the page. And the lightbox
project required a collection of thumbnail images and links.
Classically, when you load a page from the web server, complete with
images, CSS files, and other associated resources, the connection is closed
and the browser deals with what it has got. With JavaScript, however, you
can fetch additional resources using a process referred to as Ajax.
Ajax is a term apparently coined by James Garrett to describe a
collection of technologies that allow the browser to interact with the server
without having to reload everything from scratch.
In our case, we’ll use JavaScript to establish another connection
with the same or a different web server, allowing it to request additional
resources or send information back to the server.
You have already seen a simple form of the idea in the slide show
project. Every time you change the image’s .src property, you establish a
new connection with the server to get another image. In a primitive way,
you are also communicating back to the server in that each request for an
image can be processed and possibly logged.
Modern JavaScript now has the ability to do this in a much more
sophisticated way, with live updating both at the server end and at the
browser end.
Modern Ajax was first implemented using a special XMLHTTPRequest
object, which was later improved to add more features. This is still
available but can be tricky to implement.
Today all modern browsers implement the Fetch API, which allows you
to use the fetch function to do most of the hard work. It’s much simpler
than the older XMLHTTPRequest object.
In this chapter, we’ll explore Ajax in a number of minor projects. Each
one will use Ajax to fetch additional data from the server.
338
Chapter 10 Project: An Introduction to Ajax
339
Chapter 10 Project: An Introduction to Ajax
Timing Problems
In the following discussion, make sure that you have a page open,
such as the ajax.html page.
If you don’t have a page open, you’re likely to run into Content
Security Policy errors.
The hardest thing about working with Ajax has always been a matter of
timing. All communication with the web server takes time, and JavaScript
doesn’t like to be kept waiting.
Take, for example, the following code:
The Image object is a virtual image that will have the properties of an
image but won’t actually be displayed.
If you run this once, you’ll probably get a result of 0. If you run it again,
you’ll possibly get a more meaningful value, but not necessarily.
The problem occurs because request for the new image takes a little
while. JavaScript, not wishing to hang around, goes on to the next line
and attempts to read the width of the image before it’s available, giving a
result of 0.
We say that the image is loaded asynchronously, that is, in the
background.
To save you further waiting, the image is probably stored in the
browser cache, so next time it should load more quickly – possibly quickly
enough for JavaScript to be able to examine its width, which is why you
might get a nonzero result if you try it again.
340
Chapter 10 Project: An Introduction to Ajax
One way to get JavaScript to wait for the image is to use a callback
function:
The image’s onload property is supplied a function that will run once
the image has finished loading. This now works as expected by deferring
acting on the image.
When using Ajax, you will constantly run into similar timing problems:
you are trying to fetch a resource, such as text, but JavaScript is impatient
to carry on, so the data always arrives too late. The classic way to handle
asynchronous Ajax calls was also to use an onload event listener to defer
further processing to a callback function. With Ajax, however, callback
functions are much more complicated, particularly if you need to make
another Ajax call after that.
Modern JavaScript has addressed this problem with promises. A
promise allows you to include code that will run when it’s ready without all
the mucking about with callbacks.
Using Fetch
The fetch() function requests some data from a server and returns
a promise. A promise is a special object that allows you to add what
happens next, similar to adding a series of callback functions.
341
Chapter 10 Project: An Introduction to Ajax
A typical pattern is
// Hypothetical Example
fetch(url) // Step 1: Request Something
.then(processResponse) // Step 2
.then(processData); // Step 3
The url is where you expect to find the data. Each successive step is a
function that is sent to the .next() method. The function data will depend
on what’s happened in the previous step. Here, we’ve used the names
processResponse and processData, though you would very often use
function expressions instead.
For the fetch() function, the next step will get a response object as its
parameter, typically, though not necessarily, referred to as response. The
response object contains all sorts of details returned from the server, but
we’re usually interested in extracting the data.
You can define the processResponse() function like this:
function processResponse(response) {
return response.text();
}
The .text() method extracts the text from response. The result
isn’t actually just the text: it’s another promise object with the text. In
later sections, we’ll also use a json() method, once we find out what
that means.
We can, of course, use an arrow function expression for this:
response => {
return response.text();
}
342
Chapter 10 Project: An Introduction to Ajax
The next step will involve the actual data. The processData function
will get a single parameter with the text from the previous step. Again, we
can use an arrow function expression if the processing is simple enough:
This arrow function is doing more than just returning a value, which is
why the code is inside braces.
We can try this out in the console:
» fetch('https://pure-javascript.net/quote.txt')
.then(response => response.text())
.then(text => {
console.log(text);
});
I’ve been looking for a girl like you – not you, but a girl like you.
That’s the principle. Now we’ll put this into practice. For the project,
we’ll need work with the files ajax.html and ajax.js.
343
Chapter 10 Project: An Introduction to Ajax
The HTML file has four sections we’ll use to work with Ajax. The ajax.
js file includes the following code:
init();
function init() {
ajaxContent();
ajaxSlideshow();
ajaxLightbox();
ajaxCountries();
}
function ajaxContent() {
}
function ajaxSlideshow() {
}
function ajaxLightbox() {
}
function ajaxCountries() {
}
344
Chapter 10 Project: An Introduction to Ajax
<ul id="links">
<li><a href="ajax/aardvark.txt">Spotted Aardvarks</a></li>
<li><a href="ajax/starry-night.txt">Starry Night</a></li>
<li><a href="ajax/lucky.txt">The Lucky Country</a></li>
<li><a href="ajax/australia.txt">Australia</a></li>
</ul>
If you click on one of the links, the browser will load with the new text,
but we want to hijack the link and have the browser load the content into a
container div instead:
<div id="content"></div>
Clicking on a link should populate the div with the new content. The
content itself is a text file, but it actually contains HTML code.
We can set up the preliminary JavaScript in the function
ajaxContent():
function ajaxContent() {
let links = document.querySelector('ul#links');
let content = document.querySelector('div#content');
The links variable references the list of links, and the content variable
references the div that will be populated.
345
Chapter 10 Project: An Introduction to Ajax
function ajaxContent() {
let links = document.querySelector('ul#links');
let content = document.querySelector('div#content');
}
}
function ajaxContent() {
let links = document.querySelector('ul#links');
let content = document.querySelector('div#content');
fetch(event.target.href)
.then(response => response.text())
.then(text => {
content.innerHTML = text;
});
}
}
346
Chapter 10 Project: An Introduction to Ajax
<div id="slides">
<img width="640" height="480" src="" title="Missing Image"
alt="Missing Image">
</div>
347
Chapter 10 Project: An Introduction to Ajax
» var apple = {
name: 'Granny Smith',
shape: 'Round',
colour: 'Green'
};
» var apples = [
{ name: 'Granny Smith', shape: 'Round', colour:
'Green' },
{ name: 'Fuji', shape: 'Round', colour: 'Red' },
];
Of course, the spacing and indentation are optional. It’s only there to
make it easier to read for humans.
Note the comma after the last item. That’s probably a little careless, but
JavaScript will forgive that and politely ignore it.
The preceding code is pure JavaScript. JSON extends this notation
to strings. The word JSON is pronounced “j-son” and means JavaScript
Object Notation. The string form is very similar to an object literal, but it
does impose some very strict requirements. A JSON version of the second
example would be
348
Chapter 10 Project: An Introduction to Ajax
» var apples = `[
{"name":"Granny Smith", "shape":"Round",
"colour":"Green"},
{"name":"Fuji", "shape":"Round", "colour":"Red"}
]`;
This would also be valid for a normal JavaScript object literal but is an
absolute requirement for JSON.
Once you have the JSON text, you can normally convert it to a proper
JavaScript using JSON.parse(...):
» var apples = `[
{"name":"Granny Smith", "shape":"Round",
"colour":"Green"},
{"name":"Fuji", "shape":"Round", "colour":"Red"}
]`;
apples = JSON.parse(apples);
The JSON built-in object has just two methods: JSON.parse() converts
a string to an object, and JSON.stringify() does the reverse.
349
Chapter 10 Project: An Introduction to Ajax
» fetch('images/slides/slides.json')
.then(response => response.text())
.then(text => {
let data = JSON.parse(text);
console.log(data);
});
That would work, but the response object also has a convenient
shortcut method called .json() that will do that for us:
» fetch('images/slides/slides.json')
.then(response => response.json())
.then(data => {
350
Chapter 10 Project: An Introduction to Ajax
console.log(data);
});
function ajaxSlideshow() {
In the function, we can fetch the JSON file and process it:
function ajaxSlideshow() {
fetch('images/slides/slides.json')
.then(response => response.json())
.then(data => {
});
}
In this case, the images object will come from the fetch() function,
and the containerSelector is just the reference to the div in the HTML:
function ajaxSlideshow() {
fetch('images/slides/slides.json')
.then(response => response.json())
.then(data => {
351
Chapter 10 Project: An Introduction to Ajax
The delay is set to the default 3000, but you can change it to anything
you like, especially for testing.
<div id="catalogue"></div>
which, you’ll agree, isn’t very much. We’re going to use Ajax, in
combination with a little DOM manipulation, to populate the catalogue
and then let our library doLightbox() function do the rest.
The JavaScript code will be in the ajaxLightbox() function.
function ajaxLightbox() {
fetch('images/slides/slides.json')
352
Chapter 10 Project: An Introduction to Ajax
});
}
From here, the task is to use this data to generate the gallery.
function ajaxLightbox() {
let catalogue = document.querySelector('div#catalogue');
...
}
Remember that the data from the fetch() function will be an array of
objects. We can iterate through this array:
function ajaxLightbox() {
let catalogue = document.querySelector('div#catalogue');
fetch('images/slides/slides.json')
.then(response => response.json())
.then(data => {
data.forEach(item => {
// create gallery element
});
});
}
353
Chapter 10 Project: An Introduction to Ajax
// Not Yet
data.forEach(item => {
catalogue.insertAdjacentHTML(
'beforeend',
`<a href="..."><img src="..." title="..." alt="..."
width="160" height="120"></a>`
);
});
The new element is added beforeend, which means it’s added to the
end of the container. We’ve put the HTML string inside backticks because
we’ll need the template literal.
For the actual element, there are a lot of values to substitute. However,
we can break up the HTML string over a few lines:
data.forEach(item => {
catalogue.insertAdjacentHTML(
'beforeend',
`<a href="images/photos/large/${item.src}">
<img src="images/photos/small/${item.src}"
title="${item.caption}" alt="${item.caption}"
width="160" height="120"
>
354
Chapter 10 Project: An Introduction to Ajax
</a>`
);
});
You can write it on one line, of course, but it certainly won’t fit in
this book.
Once you’ve got the gallery populated, you can send it off to the
doLightbox() function:
function ajaxLightbox() {
let catalogue = document.querySelector('div#catalogue');
// fetch ...
doLightbox('div#catalogue');
}
function ajaxLightbox() {
let catalogue = document.querySelector('div#catalogue');
fetch('images/slides/slides.json')
.then(response => response.json())
.then(data => {
data.forEach(item => {
catalogue.insertAdjacentHTML(
'beforeend',
`<a href="images/photos/large/${item.src}">
<img src="images/photos/small/${item.src}"
title="${item.caption}"
alt="${item.caption}"
width="160" height="120"
355
Chapter 10 Project: An Introduction to Ajax
>
</a>`
);
});
doLightbox('div#catalogue');
});
}
You’ll probably notice that the gallery has more images than in the
original version. That’s one of the benefits of this technique: the gallery has
become more flexible.
Accessing a Database
One of the most powerful applications of Ajax is when the data is dynamic
in origin. This typically comes from a database on the server end.
You can’t access the database directly using JavaScript. What you can
do is make a request to the database server via the web server. The idea
is that the web server is expected to be running some sort of application
listening for such requests.
You won’t be able to try this on the sample site if you’re running it in a
light web server such as the Micro Web Server. If you happen to be running
it through something more powerful, such as XAMPP, or even a full web
server, then there’s a PHP script that will do the job.
In this section, we’ll assume that you’re not running such a web server.
Instead, we’ll rely on an application on the live site.
A service provided for access to Ajax is typically referred to as an
API, which is a very dated term. It means an Application Programming
Interface. In reality, it just means that it will accept correctly formatted
requests and return some sort of result.
356
Chapter 10 Project: An Introduction to Ajax
In this example, we’ll work with an API that supplies some information
about countries around the world.
The service we’ll be using will return JSON formatted data. That’s
not always the case. A common alternative format is XML, which
JavaScript doesn’t handle without a lot of extra work.
Not all APIs are free to use for one and all. Some require registration,
and some require money to change hands.
In any case, accessing data on another server isn’t always allowed.
The server needs to be set up to allow requests from other origins.
Those that offer an API will naturally have done that.
In many cases, you also need to prove some sort of key to identify
the user.
In the live sample, the URL is
https://pure-javascript.net/resources/countries.php
357
Chapter 10 Project: An Introduction to Ajax
You’ll find that the URL by itself returns a JSON string, which is a list of
country ids and names.
[
{ "id": "af", "name": "Afghanistan" },
{ "id": "al", "name": "Albania" },
{ "id": "dz", "name": "Algeria" },
...
{ "id": "zm", "name": "Zambia" },
{ "id": "zw", "name": "Zimbabwe" }
]
Your browser may interpret the JSON to give you more of a data-
oriented view. You may be able to view the raw data to see the actual
JSON string.
You can test the query string by putting it after the URL:
https://pure-javascript.net/resources/countries.php?id=au
{
"id":"au",
"name":"Australia",
"local_name":"Australia",
"capital":"Canberra",
"alpha3":"AUS",
"tld":"au",
"numeric":"036",
"phone":"61",
"continent":"Australia",
"population":"22751014",
"area":"7741220",
358
Chapter 10 Project: An Introduction to Ajax
"coastline":"25760",
"currency":"AUD"
}
<form id="country-details">
<table>
<tbody>
<tr><th><label>Select Country</label></th>
<td><select name="id"></select></td>
</tr>
<tr><th><label>Local Name</label></th>
<td><output name="local-name"></td>
</tr>
<tr><th><label>TLD</label></th>
<td><output name="tld"></td>
</tr>
<tr><th><label>Continent</label></th>
<td><output name="continent"></td>
</tr>
<tr><th><label>Currency</label></th>
<td><output name="currency"></td>
</tr>
</tbody>
</table>
</form>
359
Chapter 10 Project: An Introduction to Ajax
The form elements are mostly output elements for the received
data. However, the form includes a select element that will appear as a
drop-down menu. Currently the select element is empty: we’ll need to
populate it with JavaScript.
For a normal select element, the items would be option elements:
<select name="fruit">
<option value="a">Apple</option>
<option value="b">Banana</option>
<option value="c">Cherry</option>
</select>
If the form were to be submitted, the value of the select item would be
the value of the selected option.
function ajaxCountries() {
let countriesForm
= document.querySelector('form#country-details');
}
360
Chapter 10 Project: An Introduction to Ajax
function ajaxCountries() {
let countriesForm
= document.querySelector('form#country-details');
countriesForm.elements['id'].append(
new Option('Select a Country ...', '')
);
}
function ajaxCountries() {
let url = 'https://pure-javascript.net/resources/
countries.php';
...
}
That will simplify the code later when we come to use it.
Next, we can fetch the list of countries from the URL:
function ajaxCountries() {
let url = 'https://pure-javascript.net/resources/
countries.php';
let countriesForm
= document.querySelector('form#country-details');
361
Chapter 10 Project: An Introduction to Ajax
countriesForm.elements['id'].append(
new Option('Select a Country ...', '')
);
fetch(url)
.then(response => response.json())
.then(data => {
// Add options
});
}
The fetch() function will fetch a JSON object with the list of country
ids and names, which we’ll turn into option elements.
We can iterate through the data array and use new Option() to add the
option elements. The text will be the name property of each item, while the
value will be the id property:
function ajaxCountries() {
...
fetch(url)
.then(response => response.json())
.then(data => {
data.forEach(item=> {
countriesForm.elements['id'].append(
new Option(item.name, item.id));
});
});
}
If you try this now, you’ll see the drop-down menu populated with the
list of countries.
The next part will be to populate the rest of the form when you select a
country from the menu.
362
Chapter 10 Project: An Introduction to Ajax
Acting on a Selection
Form elements can have a change in value, and when they do, they trigger
a change event. That’s another reason why the menu should start with an
empty value: this means that there’ll always be a change event, even for the
first real value.
You can add the event listener simply by assigning the .onchange event
property:
function ajaxCountries() {
...
};
}
On change, there will be another fetch using the current value of the
drop-down menu:
function ajaxCountries() {
...
Note that we’ve put the string into a template literal so that we can use
the url variable as well as event.target.value.
363
Chapter 10 Project: An Introduction to Ajax
The query string is constructed from the string ?id= and the current
value of the selected country item.
When we have the results from the fetch() function, we can populate
the rest of the form:
function ajaxCountries() {
...
364
Chapter 10 Project: An Introduction to Ajax
function ajaxCountries() {
...
function ajaxCountries() {
let url = 'https://pure-javascript.net/resources/
countries.php';
let countriesForm
= document.querySelector('form#country-details');
countriesForm.elements['id'].append(new Option('Select
...',''));
fetch(url)
.then(response => response.json())
.then(data => {
365
Chapter 10 Project: An Introduction to Ajax
data.forEach(item=> {
countriesForm.elements['id'].append(
new Option(item.name,item.id));
});
});
Summary
In this chapter, we had a look at using Ajax to fetch additional data from
the web server. We had already fetched additional data in the form of
images in the slide show and lightbox gallery. Here, we looked at fetching
text and more complex data.
366
Chapter 10 Project: An Introduction to Ajax
Using JSON
JavaScript supports an object literal format that allows you to define an
object or an array of objects directly in code.
The JSON string format is a string that resembles an object literal
and can be converted to a real object or array of objects using built-in
JavaScript functions. The real benefit of JSON is to be able to represent
complex data as a single string. A string is easily saved in a text file or
transmitted over the Internet, whereas complex data is not so readily
managed.
The JSON data can come from a saved file on the server, but it can
also be generated dynamically on the server through some server-side
processing, such as with PHP and a database.
JavaScript has a built-in function, JSON.parse(), which will convert
JSON strings. However, the fetch() response has its own method,
.json(), which can do the same with JSON strings received.
367
Chapter 10 Project: An Introduction to Ajax
Ajax APIs
There are many third parties that offer a data service in the form of Ajax
APIs. These services often return a JSON string, though some use other
forms of data, such as XML.
Accessing a third-party API requires the other server to accept
connections. Some will restrict access to registered, and possibly
paying, users.
368
APPENDIX A
To get the fullest experience, it’s better to run your project through a
web server.
Many web developers have already installed either XAMPP or MAMP,
or something like them. They are full web servers, complete with PHP and
a database, and possibly a few other services.
If you don’t have one installed, you can use a much lighter web server
for developing your JavaScript (and HTML and CSS for that matter).
The simplest solution is to download one called Micro Web
Server from
https://github.com/manngo/micro-web-server
Macintosh Windows
To set it up:
When the server is started, you’ll see a link to the site, something like
http://localhost:8000
PHP
PHP can implement a simple web server from the command line. You can
get the details from www.php.net/manual/en/features.commandline.
webserver.php.
cd wherever
php -S localhost:8000
370
Appendix A Running a Web Server
Python
Python also has a built-in web server that can be run from the command
line. You can get the full details at https://docs.python.org/3/library/
http.server.html.
cd wherever
python3 -m httpd.server
Node.js
Node.js doesn’t have a simple one-liner, but there are a few that are ready
to go. Here are two you might like to try:
https://github.com/http-party/http-server
https://github.com/tapio/live-server
Pulsar or VSCodium
Both Pulsar and VSCodium (or VSCode if you prefer) have extension
packages that will allow you to run your project as a web server.
https://web.pulsar-edit.dev/packages/atom-live-server
371
Appendix A Running a Web Server
https://open-vsx.org/extension/ritwickdey/
LiveServer
372
APPENDIX B
Deferring Scripts
There are two main methods for adding JavaScript to your web page. First,
you can add the script in the page itself using a <script> tag.
The second method is to add a reference to an external file:
<head>
...
<script src="..."></script>
...
The src attribute references the file. The rest of the element is empty –
if you add code inside, it will be ignored. The reference is normally in the
head element.
You can get a problem with the way the browser loads the script. First,
when the browser gets to the script reference, it then pauses rendering the
page while it fetches the script. This is different to how it loads an image,
which it does in the background.
Second, the browser pauses again while it executes the script. Not only
does this interfere with rendering the page, it can also result in errors. It’s
likely that the JavaScript references part of the page that hasn’t loaded or
been rendered yet, so all of that will fail.
Modern browsers have solved this problem with two keywords: async
and defer. The async keyword will solve the first problem in loading the
script in the background while it’s working on the HTML. However, it will
still pause the rendering while it then processes the HTML. It doesn’t solve
the problem of referencing parts of the page that aren’t there yet.
The defer keyword acts like async in that it loads the script in the
background. However, it then waits until the browser has finished with the
HTML before it executes, thus solving the second problem as well.
Figure B-1 describes the process.
You would never use both. The defer keyword implies async.
Older Methods
The defer keyword has been available for a very long time, but that doesn’t
mean that you won’t see alternative methods in the wild.
The most common older method is to run the code as an event listener
function. The event was typically the load event:
window.onload = init;
function init() {
...
}
374
Appendix B Deferring Scripts
The load event is triggered when the page has fully loaded, complete
with all of the additional resource, such as CSS files and images.
Of course, the function isn’t necessarily called init().
You will also see this technique with a function expression:
window.onload = function () {
...
};
This was the dominant technique for many years. You may see it using
the addEventListener form:
For the most part, you don’t really need to wait until absolutely
everything has finished loading. The document object has an event that is
triggered when the content has finished loading, but not necessarily the
additional resources:
375
Appendix B Deferring Scripts
There’s one additional technique that you’ll sometimes see used. Some
developers prefer to put the script reference at the end of the HTML to
make sure that the browser doesn’t see it until last. That was never really
necessary, and it starts to interfere with the logical structure of your code.
376
APPENDIX C
Further Expertise
The only way to really develop expertise is, of course, to just keep writing
and to keep reading other developers’ code and tips. However, there’s also
plenty of online help available.
Here are a few places you can go for help:
• MDN Web Docs: https://developer.mozilla.
org/en-US/
378
Index
A, B C
post-increment (a++) operator, 12 callback function, 341
addEventListener() function, 141 caption property, 162
.addEventListener() method, 235, Cascading Style Sheets (CSS)
236, 245 adding HTML, 213–214
addEventListener() method, definition, 211
220, 245 disabling style, 214–215
Ajax dynamic changes, 229–230
accessing database example, 222, 224
API, 357–365 JavaScript, 213
web server, 356 properties/classes, 230, 246
CSS files, 338 resetting properties, 224–225
fetch, 341–344 sample file, 212
JSON, 367 storing state, 225–228
lightbox gallery, 352–356 styles property, 223
selected count, 344–346 transitions, 247
slide show, 347–351 working, 231–233
timing problems, 340–341 Child nodes, 99
XMLHTTPRequest object, 338 Child selector, 105
alert() function, 43, 72, 91 clearInterval() function, 139
appendChild() function, 115 Commenting the
Argument, 40 code out, 21
Arithmetic operations/operators, 9 Comments, 4, 90
Array, 44 JavaScript interpreter, 20
Arrow function, 235 nested, 22
Arrow function expression, 109, 153 types, 20–21
Assignment statements, 57 uses, 21
380
INDEX
JavaScript G
adding rules, 244
.getItem() method, 200
code, 243
Guessing game
creating style sheet, 243–244
cancelling game
JavaScript transition, 239–242
comparing equal
nested event targets, 219, 221
values, 80–81
property, 142
prompt, 79
remove, 216
retur value, 82
stop propagation, 222
consolidating message, 73–74
transition, 237–239
display limits, 83–87
function library, 87–88
F initializing data, 65–66
main play
fetch() function, 341–342, 351, 367
reporting result, 71–72
forEach() function, 120, 288
testing code, 72–73
.forEach() method, 109, 202, 280
testing response, 69–71
forEach() method, 254–255
user input, 68
format() function, 196
organizational principles, 90
format() method, 195
outlining script, 64–65
Forms
playGame function, 89
data validation, 163
preliminary code, 60–61
HTML attributes, 163
random number, 75–78
Javascript, accessing form
repeating code, 66–67
CSS, 170
starting game, 61–64
data, 171–172, 175
elements, 170–171
submit button, 172–174
H
types of elements, 164, 166–169
Functions hide() function, 297
dialog box, 42, 44 Hijacking anchors, 332
expression, 151, 235 HTML elements
parameter, 40 creating/manipulating
programming language, 39 elements, 120
void, 41 add content, 112
381
INDEX
382
INDEX
M P
.matches() method, 273 Parameter, 40
Math.floor() function, 76 Parent node, 98
Math.random() function, 75, 87 parseFloat() function, 37
Micro Web Server, 356 parseInt() functions, 87, 186
Multi-line blocks, 91 parseInt(string) function, 37
playGame() function, 64, 77
PMT(), 178
N Post-increment operator, 132
Nested lists, 264 Post test, 67
next() function, 131, 142, 158, 325 Pre-increment operator, 132
Node.js, 371 preventDefault() function, 289
Node list, 108 processData function, 343
Nodes, 329 prompt() dialog box, 91
Nullish coalescing operator, prompt() function, 68, 91, 308
228, 247 Pulsar and VSCodium, 371
Numbers Python, 371
arithmetic, 24–25
383
INDEX
384
INDEX
385