Session 21: Adding Search to a Web Page

also: Working with Lists in the DOM

Download this starter code to use throughout the session.

Opening Exercise

Last week, we learned how to write while statements that repeat actions. For example, we can display the numbers 0 through 4 with:

let i = 0;
while (i < 5) {
  console.log(i);
  i++;
}

In this week's reading, you see how to use a loop of the same form to loop over all the elements in a collection:

let allPs = document.querySelectorAll('p');
let i = 0;
while (i < allPs.length) {
  console.log(allPs[i].innerText);
  i++;
}

We have also seen in Session 17 and Session 19 how to use the forEach() function to call a function with every element in a collection.

Let's use one of these skills to style a page. Open the empty script.js file in the university-cities directory.

Write a script that changes all of the names in the City column to use gold text on a purple background.

Recall that Panther Purple is rgb(80, 7, 120) and Old Gold is goldenrod, rgb(255, 181, 0).

Open this web page to see your code in action. It loads script.js.

Hints:

A Solution

We can experiment in the console to figure out the key parts of a solution:

let cities = document.querySelectorAll('tbody tr');
let city = cities[0].querySelector('td');
city.style.color = 'rgb(255, 181, 0)';

Using those lines as a base, here is a possible solution.

let cities = document.querySelectorAll('tbody tr');
let i = 0;
while (i < cities.length) {
  let city = cities[i].querySelector('td');
  city.style.color = 'rgb(255, 181, 0)';
  city.style.backgroundColor = 'rgb(80, 7, 120)';
  i = i + 1;
}

Enter the code and reload your browser.

How can we write this code using forEach()? Put the code for processing each item in a function:

function colorCityName(row) {
  let city = row.querySelector('td');
  city.style.color = 'rgb(255, 181, 0)';
  city.style.backgroundColor = 'rgb(80, 7, 120)';
}

let cities = document.querySelectorAll('tbody tr');
cities.forEach(colorCityName);

Edit the code and reload your browser.

This approach saves us all of the work of setting up the while and its index variable, at the cost of writing a function. I prefer this way to think about the problem, but YMMV.

If you prefer the while statement approach, try following the pattern of other while statements until you become more comfortable writing your own.

Our work the last three weeks have been a mixture of learning basic JavaScript tools and using them to modify web pages, including pages with dynamic behavior.

It's little like "wax-on, wax-off" for you Karate Kid fans. The work we have been doing gets us to today and the rest of the course.

Repeating Actions

Last time, we began to write code that repeats actions. Such code is commonly called a loop. We learned that there are two kinds of loop. A definite loop is one where we know up front how many times we want to repeat the action. An indefinite loop is one where we don't.

The while statement is JavaScript's primary tool for writing indefinite loops:

while (need to continue) {
  // execute these statements
}

When writing a while statement, we often create a sentinel variable that guards the loop. We initialize the sentinel before the loop starts. The loop checks the value of the sentinel to see if it needs to continue. Inside the loop, we change the value of the sentinel variable, either based on user input or on some action taken by the loop.

A while statement is a useful tool for writing code to ensure that data meets the conditions we need, which we often call data validation code. For example:

function getPositiveNum() {
  let answer = prompt('Enter a positive number:');
  while (answer <= 0) {
    answer = prompt('Number must be positive.  Try again:');
  }
  return answer;
}

We don't know how many times to repeat the prompt. The code must do so until the input meets our requirement.

while can be used to implement a definite loop, too. The counted loop for our opening exercise is quite definite: We know that it will run num times. To execute the loop exactly num times, we do three things:

Every definite loop has to do the same three steps, in some form or another. To make this kind of code clearer, JavaScript also provides a for statement. It combines these three actions into one line:

// while statement

let i = 1;
while (i < num) {
  do-some-action
  i++;
}

// equivalent for statement

for (let i = 1; i < num; i++) {
  do-some-action
}

One advantage of the for statement is that it is harder to forget to increment the sentinel. We have to remember to do that at the bottom of our while statements.

What happens if we forget to increment the sentinel? The loop never stops running. You will have to reload the web page, or close the browser entirely, to stop JavaScript from doing the loop's action.

You can write while statements for all of your loops, or use for statements for your definite loops. In any case, you will want to be able to read for in other peoples' code.

Collections of Objects in the DOM

We have been accessing elements and their attributes, including their text content and their styling.

Thus far, we have mostly been limited to accessing only the first element to match a selector, using document.querySelector().

Then we saw the classList attribute and learned that it could contain more than one class — and that we could add a class to the list. This introduced us to the idea of a collection of items.

This week's reading discusses two kinds of collection we can get from the DOM: an element's classList and a list of all elements that match a selector.

Both share many features with JavaScript's array type.

Step through hobbit example in the reading.
Write a for loop to log all items to the console:
for (i=0; i < hobbits.length; i++) {
  console.log('Hello, ' + hobbits[i] + '!');
}

The DOM gives us many collections in values that behave much like arrays, especially in our ability to access items with [] and loop over the collection.

One of the most powerful is the querySelectorAll() method:

let paragraphs = document.querySelectorAll("p");

The object returned by querySelectorAll() is a collection: a list of elements in the DOM that match the given selector. We can work with them in the same way we work with arrays, both to get and set their values.

paragraphs[1]
paragraphs[3].innerText
paragraphs[3].innerText = 'This is so a paragraph.'
for (i=0; i < paragraphs.length; i++) {
  let str = paragraphs[i].innerText;
  console.log(str.slice(3,8));
}

Try the same with td elements in university cities table.

let cells = document.querySelectorAll('td');
cells[9].innerText
cells[9].innerText = 'Our Fair City';

Let's do something useful with our newfound powers.

Adding Search Functionality to a Web Page

A common use of scripting in web development is to change the styling of a page by adding a CSS class to am HTML element. Let's do that now. We'll add a class to many, but possibly not all, elements on a page. This will require a loop and an if statement.

I am a big fan of the work of author Kurt Vonnegut. Many years ago, I spent a few weeks one summer fun time tabulating The Books of Bokonon, a fictitious holy book created by Vonnegut in his novel Cat's Cradle. It was a simple document with no styling and a lot of text from the book. Even today, people all over the world find it on the web and email me. I've always thought it would be cool to make it more useful to readers by adding search functionality.

(Fans of Bokonon are, um, intense. Not that I can judge.)

Next week, we will talk about user inputs and events. But we already know enough to add search using a script with a prompt.

My basic idea is:

  1. Get a search term from the user.
  2. Examine all of the text elements on the page.
  3. If the element contains the user's search term, add a class to the element which highlights the element on the page.

Look at the HTML document.

Most of the text appears in p, li, and dd elements, so let's focus there. We could perhaps look at dt elements and the various headings, too.

Look at the CSS document.

The basic styles re-create the page much as it looked only with HTML, but now with CSS to handle spacing and centering. I added a class to highlight the search results.

Let's highlight one item in console, to see how it looks:

document.querySelector('li')
document.querySelector('li').classList
document.querySelector('li').classList.add('highlight')

Start with an empty script file.
Build version 1.

This script to prompt for a search term and loops over all of the li elements on the page.

Use 'vin-dit' as search term.

To process the other kinds of elements that contain text, we have to copy and paste the call to highlightMatch.

Do this for 'p'.

Repeating things in this way is a job for... another loop!

Build version 2.

For this version, we create another function that takes a selector as an argument, finds all the elements that match the selector, and calls the highlightMatch function on each element. The result is a loop inside another loop, but with the second loop in a function.

Try the new search functionality with 'foma' and 'karass' as search terms.

Searching for 'foma' points us toward an improvement. The search found multiple matches, but it missed the dictionary entry that defines the term, because there the word is capitalized. We will have a similar problem if we search for 'pabu', and not 'Pabu'. Google has trained users not to capitalize even proper names...

The solution is to normalize the text, by working in all lowercase. We can use a new string method from this week's reading to accomplish this goal:

let eText = element.innerText.toLowerCase();

We should probably lowercase the search term, too, in case a stickler for proper capitalization comes to the page.

target = target.toLowerCase();

Step back. We have added so much functionality with so little code! We wrote one for loop that we use to process several collections. We added a class with simple CSS to style the results.

You are learning some powerful tools.

Note: If you would like to see how to write this script using a traditional for loop, check out this Version 1 and this Version 2.

Closing

Homework 9 asks you to write more JavaScript, including functions and if statements.