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.
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:
-
Use
querySelectorAll('tbody tr')
to select the rows with city names in them. -
Write a
while
loop that processes each row. -
Use
querySelector('td')
to select the first column in the row. Change its color via itsstyle
property.
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; }
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);
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:
- Before the loop starts, we initialize the sentinel to 0.
- In the loop condition, we check to see if the sentinel has reached the maximum value.
- At the bottom of the loop, we add 1 to the sentinel.
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 afor
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)); }
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:
- Get a search term from the user.
- Examine all of the text elements on the page.
- If the element contains the user's search term, add a class to the element which highlights the element on the page.
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.
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')
This script to prompt for a search term and loops over all of
the li
elements on the page.
To process the other kinds of elements that contain text, we
have to copy and paste the call to highlightMatch
.
Repeating things in this way is a job for... another loop!
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.
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.