Week 10: More JavaScript, Including if Statements

Table of Contents

We dive deeper into JavaScript this week, revisiting two topics and introducing a new topic. The links in the following list are to sections on this page.

Download this starter code to use throughout the reading.

The nth-child() Pseudo Class

Often we don't have the luxury to select elements by their type, class, or ID, especially when we write JavaScript programs to manipulate web page content. There are many powerful CSS selectors that can help us in these situations, and one of them is the nth-child() pseudo-class. We encountered this selector briefly in Session 11 and Session 13. Let's learn more about it now.

Consider this web page with a table styled by this CSS file.

This selector matches a number of child elements whose numeric position in the series of children matches the argument. We can use a number there, such as 3, or a keyword, such as 'even'.

We can write more complex arguments to nth-child(). This selector matches a number of child elements whose numeric position in the series of children matches the pattern an + b, or just n when a=1 and b=0. For example, you could use it to select every fifth row in a table, every third cell in a row, or every 42nd paragraph on a page.

See the MDN documentation for more details.

We can also use this pseudo-class as a selector in our JavaScript. The querySelector() method returns only the first occurrence of an element that matches the selector. Often that is exactly what we need in a script.

Consider the table. Ames is the third row of the table. Its population is second item in the row. How might we change the color of that number red?

How do we select the third row in the table? tr:nth-child(3).

How do we select the second table cell? td:nth-child(2).

Can we select the second table cell in the third row directly? Yes. At times this semester, we have seen that CSS can select an element that is inside another element by combining the selectors with a space. For example, we can select all the paragraphs in a section with:

section p { ... }

This is called a descendant selector, which has some other useful features.

We can use descendant selectors in our JavaScript, too. So, to select the second cell in the third row of a table directly, we can do this:

> document.querySelector('tr:nth-child(3)')                   // third row
<tr>
  <td>Ames</td>
  <td>67,282</td>
  <td>8</td>
</tr>

> document.querySelector('tr:nth-child(3) td:nth-child(2)')   // second cell in third row
<td>67,282</td>

> let item = document.querySelector('tr:nth-child(3) td:nth-child(2)');
> item.style.color = 'purple';

nth-child() and descendant selectors can both be very handy in our CSS and in our JavaScript.

Practice Exercise: nth-child()

Open the script file nth-child.js in VS Code and:

Write a script that:
  • prompts the user for a column number,
  • prompts the user for a color,
  • finds the td cell in the user's column, and
  • changes its backgound color to the user's color.

Test your script on the university cities page, which loads the script file.

This script will have to build a string for the selector. For example, if the user enters '3' for the column number, the script will construct: 'td:nth-child(3)'.

You can build the string with the + concatenation operator, using two literal strings and the user's entry.

A Solution, and a New Goal

Our script needs to follows the process outlined in the spec:

  1. Prompt the user for a column number.
  2. Prompts the user for a color.
  3. Find the td cell in the user's column.
  4. Set the element's backgound color to the user's color.

In this case, we can write three of those steps with one statement of JavaScript each. The third step might be easier if break it down into two steps:

  1. Prompt the user for a column number.
  2. Prompts the user for a color.
  3. Find the td cell in the user's column:
    1. Build the selector
    2. Query the document for the matching element.
  4. Set the element's backgound color to the user's color.

Now the third step seems approachable, too. Here is a possible solution in JavaScript:

let column = prompt('Column number?');
let color = prompt('Color?');

// Find the td cell in the user's column
let selector = 'td:nth-child(' + column + ')';
let cell = document.querySelector(selector);

cell.style.backgroundColor = color;

Try it with '3' and 'lightblue', or '1' and 'green'. All works well. The argument to document.querySelector() is a string, which can build as easily as we can type. This gives us a lot of freedom to create interactions.

Now try it with '4' and 'lightblue'. What happens?

Nothing happens on the web page, because there isn't a fourth column in the table. If we open the developer tools, we see that there was an error while executing the script. The

  TypeError: null is not an object (evaluating 'cell.style')

on Line 5 of the file. Click on the line number, and the tools take us to the offending line. The error actually occurred on Line 4, when document.querySelector(selector) returned null, because td:nth-child(4) is not a valid selector in a table of three columns.

Wouldn't it be nice if we alerted the user to the error, rather making them guess, or troll around in the developer tools? To do so, the script needs to validate the column number before proceeding with the rest of its steps:

  1. Prompt the user for a column number.
  2. Check if number is a valid selection.
  3. If valid:
    1. Prompts the user for a color.
    2. Find the td cell in the user's column:
      1. Build the selector
      2. Query the document for the matching element.
    3. Set the element's backgound color to the user's color.

Let's learn the JavaScript we need to make decisions such as "Is this number valid?".

The if Statement

In JavaScript, we make decisions with an if statement. The simplest form of if statement looks like this:

if (expression) {
  // execute these statements [A]
}

// resume flow and execute these statements [B]

An if statement works like this:

For example:

if (x > 1) {
  console.log('greater');
}

console.log('moving on');

Try this code with x = 3 and x = -1 and x = 1.

We can use any expression that evaluates to true or false as the test expression. We will explore test expressions later in the reading.

The if-else Statement

We can also make either/or decisions with an if statement by adding an else clause:

if (expression) {
  // execute these statements [A]
}
else {
  // execute these statements [B]
}

// resume flow and execute these statements [C]

An if-else statement works like this:

For example:

if (x > 1) {
  console.log('greater');
}
else {
  console.log('not big enough');
}

console.log('moving on');

Try this code with x = 3 and x = -1 and x = 1.

With an if-else statement, either the 'then' part is executed or the 'else' part is executed. One of the options will always be executed.

An if-else statement is just what we need to implement the new logic for the script to solve the opening exercise:

  1. Prompt the user for a column number.
  2. Check if number is a valid selection.
  3. If valid:
    1. Prompts the user for a color.
    2. Find the td cell in the user's column:
      1. Build the selector
      2. Query the document for the matching element.
    3. Set the element's backgound color to the user's color.
  4. Otherwise: alert the user.

We can ensure that the user does not enter a column number that is too large. Here is the new code:

let column = prompt('Column number?');

if (column < 4) {
  let color = prompt('Color?');
  let selector = 'td:nth-child(' + column + ')';
  let cell = document.querySelector(selector);
  cell.style.backgroundColor = color;
}
else {
  alert('The column number must be 1 through 3.')
}

Try it out. Our code now checks to see if the column number is too big.

But what if the user enters a 0, or a negative number? We need a way to write a different kind of boolean expression that combines two expressions. Let's learn that next.

Boolean Expressions

The test expression for an if statement must evaluate to true or false. This kind of expression is called a boolean expression, after the 19th-century mathematician George Boole, who codifie many rules that govern true/false expressions.

The test expression can be:

One way to write a boolean expression is to compare two expressions to get a true/false value. a < b evaluates to true if a is less than b, and false otherwise.

These are the common comparison operations:

These operators compare values of any type (say, numbers or strings) and give a boolean value. For example, 5 > 3 evaluates to true, 5 < 3 evaluates to false, and 'Eugene' === 'Eugene' evaluates to true.

JavaScript also provides operators that work with boolean values themselves. They are called logical expressions.

expression1 && expression2 means "expression1 and expression2". It evaluates to true if both expression1 is true and expression2 is true.

expression1 || expression2 means "expression1 or expression2". It evaluates to true if either expression1 is true or expression2 is true.

We can also negate a boolean expression using the ! operator. ! expression means "not the value of expression". It evaluates to true if the expression is false, and false if it is true.

Finishing the Opening Exercise

The && logical operator is just what we need to complete the nth-child exercise. To be a valid column number for the cities table, the value must be both greater than 0 and less than 4:

if ( (column > 0) && (column < 4) ) ...

Remember: the entire test expression must be enclosed in a pair of parentheses.

Try it one last time with several column numbers: 1, 3, 0, 4, -2, 12.

Success: the final code.

Perhaps, though, we would like to improve the script one more time... Rather than alerting the user that a column number is out of range, we could ask the user to try again. We will need to write a boolean expression to do this, but we also need a way in JavaScript to do something again. This will be one of our topics for the next session or two.

Passing Values to Functions

Now we switch to another topic altogether.

We have been writing functions now every day for several sessions, both so that we can call them and so that we can give them to elements in the browser, to be called in response to a click in the window. Let's make sure we understand how functions work.

Recall that a function is

We define a function using the function keyword:

function showDistance(speed, time) {
  alert(speed*time);
}

We call a function by using its name with a pair of parentheses:

showDistance(10, 5);

How do we know if a function requires us to give it some data when we call it? We look at the function definition to see if it lists any parameters. In the showDistance function, speed and time are the parameters. They tell us that showDistance requires two pieces of information to do its job. Their names suggest that they are a speed and an amount of time.

When we call the function, we pass two values to the function. These values are called arguments. In the example above, 10 and 5 are the arguments.

a graphic showing how arguments are mapped onto parameters
The arguments we pass are mapped onto the parameters in the function definition.

The arguments we pass can be any expression that produces a value:

We use the names of the parameters in the function definition as variable names. These variables, though, exist only inside the body of the function. JavaScript doesn't care what names we give our parameters. We choose names that convey the meaning of the values, so that our code is easier to read and understand.

In the function, these values of the arguments are assigned to the names of the parameters, in essentially the same way we assign values to variables in our code.

a graphic showing how the values of parameters are used in a function definition
The parameters are used as variables inside the function definition, carrying the values given as arguments.

Calling a function in this way implements the same process that would have happened if we simply wrote the code as a sequence of statements, without a function.