Week 12: Dynamic Web Pages: Events and the DOM
This reading draws from a page by David Humphrey.
Table of Contents
This week we will use JavaScript in two ways:
- to add more dynamic behavior to our web pages, using "mouse over" events to implement a photo gallery
- to generate HTML for lists and tables that woule be tedious to type in ourselves
This reading prepares us for both tasks.
In order to change a web page, we need to know a bit more about how to use JavaScript to interact with the DOM. These sections revisit and expand upon two topics we have been learning about in recent weeks.
To use JavaScript to transform create elements and add them to a web page, we need learn a few new techniques:
If you are short of time : If you have time read only part of this page, focus on the last two sections, Creating Elements and Modifying the DOM and A Couple of Ideas on Strings and Arrays. They present the most new materialfor our use in class this week.
Events and Event Handlers
Introduction
Programming the web is event-driven programming. The browser displays some initial content and then essentially enters an infinite loop, known as the event loop, waiting for events to occur and then responding.
Some events are caused by user actions, such as:
- clicking a button
- moving the mouse
- pressing 'enter' or another key
- changing tabs in the browser
Others originate in outside sources, such as:
- timers
- messages from other processes on the computer
- reports from sensors
Instead of writing a program that executes in a predefined order, we write functions that will be called in response to various events occurring. Such functions are often referred to as event handlers, because they handle the case of some event happening. If an event occurs for which there is no event handler, the browser simply ignores it. However, if one or more event handlers are 'listening' for the event, the browser will call each in turn.
You can think of events like light switches, and event handlers like light fixtures. Flipping a light switch on or off triggers an action in the light fixture, or possibly in multiple light fixtures at once. The fixture handles the event of the light switch being flipped.
Programming the DOM is typically done by writing many functions that execute in response to events in the browser. Each kind of event has a name. We can register an event handler by connecting our function to the named event.
In Session 18, we first added dynamic behavior to a web page with a button that changes the styling on the page. That example showed a simple way to register an event handler with an element on a web page.
In
Session 22,
we saw that we could register multiple event handlers with an
element using the addEventListener()
method.
In such a case, the browser will call two different event handlers (functions) in response to a single event (a button click). What's nice about this is that different parts of our code don't have to be combined into a single function. Instead, we can separate the logic of the two actions (changing the page versus logging) in different functions. These functions can also be created and registered in different places.
addEventListener()
is more versatile than the
older onevent
properties, so feel free to use it
if you find it useful.
Notice that, in both techniques, we associate the event handler with an event on a particular HTML element. Each HTML element can have handlers for events that happen to or with it.
The Event Handler Function
An event handler is simply a function. It typically takes
one argument, an event object that contains information about
the event that the function is handling. By convention, the
name of the parameter is often the single letter
e
.
In most of our examples thus far, the event handler is a function defined in the usual way. We register the handler with the element using the function's name. However, we can also create and pass a function in the expression that register's the handler, all in one expression. For example, we could replace the content of the script element in our button-click example to:
let button = document.querySelector('button'); button.onclick = function(e) { document.body.style.backgroundColor = "lightblue"; document.body.innerHTML = "<h1>I am a little blue today</h1>"; };
Notice the bolded code. It looks just like a function definition, minus the name. This expression is called an anonymous function, because it is nameless. In cases where we are creating a functsion solely to be an event handler, this technique can be useful.
The argument, e
, is an instance of the
Event
object.
The browser passes the event to the function so that it can
use properties of the event to control what happens next.
Sometimes, the function will not need the event object to do
its job, as in respondToClick()
. In these cases,
the function can ignore the argument.
Other times, the function will need more information to do
its job. In these cases, the function will access the event
object to get the information it needs. For example,
the logData()
function in Session 22
might access the event's timeStamp
data property
and record that value in a database.
We saw an example of an event handler that uses the event object back in Session 20's lab. There, the script file defined an event handler that watched for mouse movements. The handler function accessed the x- and y-coordinates of the mouse at the time of the event so that it could display the mouse's location:
function showMessage(e) { let mouseX = e.clientX; // the x coordinate of the mouse pointer let mouseY = e.clientY; // the y coordinate of the mouse pointer // ... }
In the button-click example, the respondToClick()
function could use the event object to get a reference to the
<button> element that was clicked. This would allow it
to change the text on the button in addition to changing the
heading and background color:
let btn = e.target; // get reference to the button btn.innerText = "You clicked Me!"; // change the text of the button
Common Events
There are many types of events we can listen for in the DOM, some of which are specialized to certain elements or objects. However, there are some common ones you will see often:
-
click
anddblclick
: when the user clicks or double clicks on an element -
mouseover
andmouseout
: when the user moves the mouse over or off an element -
keypress
: when the user presses a key on the keyboard -
change
: when the content of an element changes, for example, the user enters data into a form
We can register a handler for any of these events using
either of the techniques described above. For example, if we
wanted to listen for a mouseout
event on a
div
element, we could write code like this:
<div id="map">...</div> <script> let map = document.querySelector('map'); // register a single event handler via the on* property map.onmouseout = function(e) { /* do something in response to mouseout event */ } // register one of perhaps many event handlers via addEventListener() map.addEventListener('mouseout', function(e) { /* do something in response to mouseout event */ }); </script>
Programming the DOM
Introduction
Web pages are dynamic: they can change in response to user actions or different input data. Whereas HTML defines the initial structure and content of a page, the DOM represents the actual content of the page as it exists at a given moment in the browser. This can be something quite different from the initial HTML used to load the page.
Consider a web page like GMail. When you visit your inbox, the messages you see are not the same as when your friends visit theirs. The HTML for GMail is the same no matter who loads the page, but it quickly changes in response to the needs of the current user.
So how does one modify a web page after it has been rendered in the browser? The answer is DOM programming.
We've been using the DOM acronym a lot in recent weeks. The Document Object Model (DOM) is a programming interface: a set of objects, functions, and data properties. It allows scripts to interact with and modify HTML documents. Client-side web programming is essentially using the DOM via JavaScript to make web pages do things or respond to actions.
You may have noticed in our work with JavaScript that there
is nothing particularly web-like about it as a language. It
looks and feels like a programming language. We have written
functions, if
statements, and for
statements. We have worked with strings, numbers, and arrays.
Lots of programming languages let you do this. JavaScript
cannot do anything with the web on its own. Instead, we need
to use JavaScript to access and use the objects, functions,
and data properties made available to us by the DOM .
As web programmers, we use the DOM via JavaScript to accomplish several kinds of tasks:
- to run code in response to events triggered on the page
- to find elements in the page
- to inspect and modify elements and their content
- to create, add, and remove elements from the DOM tree
The opening section of this reading expanded on the ideas we use to do the first kind of task, events and event handling, which we first encountered in Session 18.
We've been doing the second and third kinds of task, finding and modifying elements, for many sessions. In Session 22, we revisited those tasks in more detail. The following sub-sections summarize a couple of key ideas in this kind of DOM programming.
In Session 22, we also saw a glimpse of the fourth kind of task, creating elements and adding them to the DOM tree. The next section expands on this use of JavaScript and prepares us for our work in the coming week.
Finding Elements in a Web Page
We have experience finding elements in the page and using them to access the corresponding text and styling. The reading for Week 2 introduced us to several, and we've seen a few others since. Here is a quick rundown of what is available to us:
Data Properties. Some elements can be accessed directly as data properties, including:
document.head
document.title
document.body
-
document.links
gives a list of all anchor elements (a
) on the page -
document.images
gives a list of all image elements (img
) on the page
Try the last two out on the Unsplash search page for Cedar Falls. Open the console, and you can learn that, as of , the page contains 84 images and 206 links.
Query Selectors.
document.querySelector(selector)
returns
the first element on the page matching the given selector,
and document.querySelectorAll(selector)
returns a list of all elements on the page matching the
given selector.
In fact, we can use the query selector methods on any HTML
element, not just document
. For example:
let div = document.querySelector('div'); div.querySelector('nav')
lets us select the nav
element that is under the
first div
element on the page. This lets us
focus our searches on pages with many levels of element.
Working with Elements in the DOM
Once we have a reference to an element in the DOM, we can use a number of properties and methods to work with it.
We have worked with these ideas many times already this semester, including these element properties:
-
element.id
returns the element'sid
, as in<p id="intro"></p>
. -
element.className
gets or sets the value of the element'sclass
attribute. -
element.classList
returns a collection containing all of the classes applied to the element. -
element.innerHTML
gets or sets the markup contained within the element, which could be text, but could also include other HTML tags. -
element.innerText
, which accesses only the text. element.parentNode
gets a reference to the element's parent, that is, the node that contains this element.
And these element methods:
-
querySelector()
andquerySelectorAll()
-
element.hasAttribute(name)
,element.getAttribute(name)
,element.setAttribute(name, value)
, andelement.removeAttribute(name)
. See this section of Session 18 for an introduction, and this section to see how we used them to reveal and hide content on a page. -
element.scrollIntoView()
, which we used in Session 22 to scroll into a view an element we added to a page.
Creating Elements and Modifying the DOM
Review of Properties and Methods
In addition to searching through the DOM, we can also use JavaScript to make changes to it. The DOM provides a number of methods that allow use to create new content:
-
document.createElement(name)
creates and returns a new element of the type specified byname
.let paragraphElement = document.createElement('p'); let imageElement = document.createElement('img');
-
document.createTextNode(text)
creates a text node, that is, the text within an element as opposed to the element itself.let textNode = document.createTextNode('This is some text to show in an element');
These methods create the new nodes for the DOM tree, but they do not place them in the page. To do that, we first need to find the correct position within the existing tree. We have to be clear where we want the new element to get placed in the DOM.
For example, if we want to place a node at the end of the
body
, we can use appendChild()
:
let paragraphElement = document.createElement('p'); document.body.appendChild(paragraphElement);
If instead we want to place the node within an existing
div
with an id of "content", we would find the
element and append the node there:
let paragraphElement = document.createElement('p'); let contentDiv = document.querySelector('#content'); contentDiv.appendChild(paragraphElement);
Both examples work the same way: given a parent node (say, the
document
or the div
), add the new
element by appending it to the end of the list of its children.
We can also add the node before an existing node using the
insertBefore(newNode, existingNode)
method. For
example, this code:
let paragraphElement = document.createElement('p'); let contentDiv = document.querySelector('#content'); let firstDivParagraph = contentDiv.querySelector('p'); contentDiv.insertBefore(paragraphElement, firstDivParagraph);
inserts the new paragraph before the current first paragraph
in the div
.
Removing a node works in a similar fashion, using the
removeChild()
method. For example, suppose that
we want to remove a loading spinner from a page:
// Step 1: get the spinner element let loadingSpinner = document.querySelector('#loading-spinner'); // Step 2: get the spinner's parent element let parent = loadingSpinner.parentNode; // Step 3: remove the child element from the parent parent.removeChild(loadingSpinner);
Examples
-
Add a new heading
h2
to a document: -
Create a new paragraph and insert into the document:
<div id="demo"></div> <script> // Create a paragraph element and set its text let pElement = document.createElement('p'); pElement.innerHTML = 'This is a paragraph.'; // Get a reference to the div with id = "demo" let demoDiv = document.querySelector('#demo'); // Add the new paragraph to the div demoDiv.appendChild(pElement); </script>
// Create the new element let newHeading = document.createElement('h2'); /* * Add some text to the new element we just created. * This is the equivalent to including <h2>This is a heading</h2> on the page. */ let textNode = document.createTextNode('This is a heading'); newHeading.appendChild(textNode); // Add the the new heading to the body document.body.appendChild(newHeading);
A Couple of Ideas on Strings and Arrays
The elements we add to a web page must have some content. Often, this content comes from text in a file or other data structure. Reading from a file or from a database is beyond the scope of this course, but we can use JavaScript to process text in a string and use that text to construct elements to add to our web pages. Let's learn a couple of new ideas that we can use for this sort of text processing.
Backticks and Template Strings
We have seen that JavaScript supports strings written with both single quotes and double quotes:
let firstName = 'Travis'; let lastName = "Kelce";
This gives us the ability to use the other kind of quote inside a string without having to indicate that the character is inside a string:
let claim = 'The "University of Northern Iowa" is a registered trademark.'; let quote = "'allo, mate. 'ow are ye?";
JavaScript also supports a third kind of string, using the backtick:
let holiday = `Thanksgiving`;
We don't need backticks for simple strings, but they do give us two new capabilities.
First, consider how we sometimes assemble strings out of parts. For example, on Homework 7, we assembled an IP address from its four parts:
let ipAddress = byte1 + '.' + byte2 + '.' + byte3 + '.' + byte4;
... and in Session 22, we saw a function that assembled an RGB string from three color components:
let color = 'rgb(' + value + ', ' + value + ', ' + value + ')';
Building strings in this way makes it hard to see the kind of string being created. With backticks, we can define a template string that pulls the values of one or more variables right into the string:
> `${byte1}.${byte2}.${byte3}.${byte4}` '192.168.2.1' > `rgb(${red}, ${green}, ${blue})` 'rgb(222, 141, 36)' > let lastNameFirst = `${lastName}, ${firstName}`; > lastNameFirst 'Kelce, Travis'
Template strings often communicate more clearly than the
corresponding concatenation of strings using +
.
Second, backticks allow us to create
multi-line strings.
In this code, the string stored in paragraph
extends over three lines:
> let paragraph = `But in a box beneath my bed Is a letter that you never read, From three summers back.`;
This can be handy in many situations, including when we read a file into a string for processing.
Sometimes, the separate lines are different records in a collection. Consider this string that consists of different names, one per line:
let classList = `Dee, Ruby Kay, Alan Vee, Bobby`;
Processing Strings Containing Structured Data
With a string of this sort, we can use a few methods in the
String
class to decompose the string into its
parts, and work with those parts.
Each line in a multi-line string ends with a newline
character, which is written as '\n'
. The
backslash means that the 'n' is not the character, but the
code for a newline.
We can split a string at a separator using
the split()
method.
It returns an array of substrings that lie around and between
the separators. If we split classList
using
'\n'
, we get an array with three strings, one
for each line in the string:
> let classListArr = classList.split('\n'); > classListArr [ 'Dee, Ruby', 'Kay, Alan', 'Vee, Bobby' ]
Notice that each entry in classListArr
is a name
in "lastname, firstname" format. If we want, we can split
each name into its parts, using split()
with the comma:
> let ruby = classListArr[0]; > ruby 'Dee, Ruby' > let rubyParts = ruby.split(','); > rubyParts [ 'Dee', ' Ruby' ]
With a for
statement or a call to
forEach
, we can split all of the names into an
array containing the last name and the first name.
Notice that the space following the comma is included in the first name strings. To use these names on our web pages, we would want to eliminate any spaces at the beginning and end of the strings.
The trim()
method
does the job. s.trim()
returns a new string with
leading and trailing whitespace removed from s
:
> rubyParts[1] ' Ruby' > rubyParts[1].trim() 'Ruby'
trim()
is useful for cleaning up strings that
users type into web forms, or data we get from spreadsheets.
With split()
and trim()
in hand,
we can write loops that decompose a file or a long string
into its individual parts, which can use to create, say, a
long nested list of entries, or a table made from spreadsheet
data.
We will do just that later this week.