The Document Object Model and DOM API
Over the last couple of chapters, we’ve covered some very basic HTML and CSS, and a whole lot of slightly less basic JavaScript.
In this chapter, we’ll look at how HTML, CSS, and JavaScript interact to create a web page. While doing so, we’ll show you how to manipulate your HTML document from within JavaScript.
The Developer Console
You can press the F12 key on most browsers to open the Developer Console. Try that now, and if it doesn’t work then look up the documentation for your browser and find out how to open the console.
Take some time to explore this window now. You’ll be using this a lot throughout your career as a three.js developer, or indeed while doing any kind of web development.
There’s a lot to see here, but we’ll only use two of the tabs in this book: the Elements tab, and the Console tab.
The Elements Tab
The Elements tab shows the structure of your HTML page. Here, you can view and edit HTML elements and the CSS applied to them.
The Console Tab
The Console tab displays any messages, warning, or errors generated by the current page, and you can also use this as an interactive JavaScript shell, or REPL, which is great for testing small pieces of JavaScript code.
Logging to the Console with
console.log
console.log()
allows you to log information to the console. This is the main debugging technique we’ll use in this book. It’s a simple but powerful technique, and until you start to create more complex apps it’s likely to be the only one you need. Whenever you want to check the value of a variable, or details of an object or class, you can simply log them to the console and take a look.
Here’s how to check the value of a variable called x
using the browser console:
There are a range of other console methods that we can use, such as console.warn
, console.error
, console.table
, console.time
, and so on. You can see them all on the
MDN console page.
The Document Object Model (DOM)
An HTML document is a tree structure, where
elements like <head>
, <body>
, <div>
, <p>
, and so on, are leaves of the tree:
To work with an HTML page in JavaScript, we need to create a representation of this structure as a JavaScript object, and that’s where the Document Object Model comes in.
The Document Object Model (DOM) is a representation of the HTML document modeled as a JavaScript object.
Using the DOM, we can access and manipulate the elements in an HTML page using JavaScript.
A web page that looks like this:
…is represented as a series of nested JavaScript objects that looks like this:
This is a highly simplified example. There are lots of properties and methods attached to the window
and document
objects, which we have represented here using ...
. You can open up the developer console (press F12) on any web page and type window
to explore them.
The DOM API
The DOM API is a set of interfaces that we can use in JavaScript to manipulate the DOM. We’ll find these interfaces (in plainer words, methods and properties), attached to every level of the DOM.
For example, often we want to set CSS styles on an element using JavaScript. Nearly every element in the DOM has a .style
property that we can use for this purpose. Using our simple example above, in JavaScript, we can access the style properties of the head and body like this:
We can also access the <h1>
element’s style, although it takes a little more work, as we’ll see below.
The Browser’s Global Object: Window
In the previous chapter, we explored how the
global scope is represented in the browser using the
window
object.
As we saw then, we can access window
and any properties attached to it from anywhere in our code. The DOM API is attached to window
which means we can access that from anywhere too.
The global window
object is specific to web browsers. Other JavaScript environments such as Node.js may behave differently.
Document
Another important part of the DOM API is
Document
. We can access this using window.document
(or just document
as we’ll see in a moment). This represents the actual HTML document of the current page. The rest of the elements on the page are located further down, like document.body
and document.head
.
You Don’t Always Need to Type window
Whenever we use a variable in JavaScript, the engine searches for it in the local scope (say, inside the current function). If it doesn’t find it there, it looks in the parent scope (for example, the module scope). The engine keeps searching through scopes for the variable until it reaches the final scope, which is global scope.
This means, to access a property of window
, we don’t need to type window
.
In the second line here, when we type document
, the engine searches for this value in every scope until it reaches the global scope window
, where it finds the window.document
property.
This makes window.document
and document
equivalent, so we can save a few keystrokes and leave out window
.
This applies to any custom data to you add to window
as well.
There are lots of methods and properties attached to window
, so we can avoid a bit of typing this way. For the rest of this chapter, we’ll mostly omit window
. However, this is not a rule so if at any point you feel it’s more clear to include window
in your code, feel free to do so.
document.head and document.body
document.head
and
document.body
refer the <head>
and <body>
elements in your HTML document. As we saw above, we can access and edit properties of these elements such as document.body.style
.
HTML Elements
HTML elements are represented using
the Element
class. Many of the DOM API methods that we’ll encounter throughout this chapter are attached to every element.
Accessing HTML Elements
The .head
and .body
can be accessed in a couple of keystrokes from anywhere in our code, but this is not the case for the rest of the elements on our page. This is because every valid HTML document will have both a <head>
and <body>
, so it makes sense to provide shortcuts to these. Every other element is optional, however, so we’ll have to do a little more work to access them.
Accessing Child Elements Using element.children
We can find all the child elements of a given element in the
element.children
property. Given this page:
… we’ll find the <title>
element in document.head.children
and the <h1>
element in document.body.children
.
element.children
is read-only, so we cannot use this to add or remove elements from the page.
querySelector
and querySelectorAll
Accessing the .children
property is fine for an extremely simple example like this, but web pages often consist of thousands of elements. To help us search through these and find the elements we need, the DOM API has lots of methods for selecting elements on the page.
In this book, we’ll restrain ourselves to using just two of these selector functions:
querySelector
and
querySelectorAll
. The difference between these is that querySelector
will return only the first matching element, while querySelectorAll
will return any number of matches.
If we have an HTML page like this:
…we can access the elements individually using querySelector
:
In the last two examples, we use the #
symbol to refer to an element by ID, and the .
symbol to refer to a class, as we do in CSS.
If querySelector
finds more than one matching element, it will return the first element and stop looking. Here, that means our alertElem
variable will contain only the first of the two elements with an alert
class. If we want all matching elements, we can use querySelectorAll
.
allAlertElements
is a list of all matching elements, but it’s not an array. Instead, it’s a special array-like object called a
NodeList
. You can use a NodeList
in a similar manner to an array, however, it lacks many of the built-in array methods.
Most DOM API Methods are Available on All Elements
These methods, like most DOM API methods, are available on all HTML elements. This means we don’t have to search through the whole document every time we are looking for an element. This can improve the performance of your application a lot when dealing with huge web pages.
For example, using our above document again, we can search through the document.body
for the h1
element.
Changing the Text in an Element
There are many ways to change the text of an element, but we’ll always use
element.textContent
.
Let’s use that to give our page a more interesting heading:
Find the Current Browser Window Width and Height
When using three.js we need to give our scenes a size pixels. Often, we want to match the same size as the browser window.
At any time, we can access the width and height of the browser window using
window.innerWidth
and
window.innerHeight
:
The value returned is in pixels, and you will see these values used a lot in three.js examples around the web. Any time the browser window size changes, these values will also change and we will need to update our scene. We’ll see how to
listen for resize
events in a few moments.
Find an Element’s Width and Height
We can access the width and height of an HTML element using
element.clientWidth
and
element.clientHeight
.
In this book, we’ll put our three.js scenes into a #scene-container
element, set the size of that using CSS (either the full window size or a specific part of the window), and then use the width and height of the element to size our scene.
We’ll mostly create full-screen examples throughout the book, so there’s not much difference between using the container width or the full window width. However, the usefulness of this approach becomes apparent when you need to create scenes that don’t take up the full page. In that case, we don’t need to change any code, we simply change a couple of lines of CSS instead.
There are other ways of getting information about an element’s dimensions, such as
.offsetWidth
/.offsetHeight
which takes into account any borders and padding that may have been set in the CSS, and
.scrollWidth
/.scrollHeight, which also includes parts of the element that may be off-screen.
Creating New Elements
There are many ways to create new HTML elements, but we’ll stick to
document.createElement
to do this:
Adding Elements to Our Page
To add the newly created element to our page, we can use
.append
.
Let’s add our newly created <h2>
as a child of the <body>
element:
Once added this way, our new headingElem
will be immediately become visible, positioned after the <h1>
that we created in HTML.
There’s a similar method called
.appendChild
, that works almost the same way as .append
. There are some technical differences between these two methods, but for our purposes, they are not important. We’ll simply choose .append
since it saves a few keystrokes.
Listening for Events
One of the most important functions of a web application is reacting to change. In JavaScript, changes are called Events, and there’s a huge list of events to choose from.
One important category of events is user input such as
clicks,
key presses,
scrolling the mouse
wheel, screaming, and so on. OK, not that last one. There are many other events returned by the browser, for example, when media such as video has loaded and is ready to play, or has finished playing, events related to CSS transitions and animation, and even events related to printing the current webpage. Another important event which we’ll look at in a moment is
the resize
event. We’ll use this to update the size of our three.js scenes whenever the browser window changes size.
When we’re talking about events, we sometimes refer to HTML elements as event targets.
The addEventListener
Method
To listen for events, we’ll use something appropriately called an event listener. We can attach event listeners to nearly any HTML element, from the window
itself, to the document
, to sub-elements like headings, paragraphs, and buttons. If we attach an event listener (for example, a listener for mouse click
events) to window
or document
, events will be captured on the whole page. However, if we attach the listener to a single element such as a button, the click
events will only be captured when the button is clicked.
To attach an event listener to an element, we use the
element.addEventListener
method. This takes three arguments: the event type to listen for, a
callback function specifying what to do when the event occurs, and optional options. We’ll leave out the options here for the sake of brevity.
As another example, let’s say we have a button defined in HTML:
We can listen for clicks on the button like this:
The event
Argument
The callback function receives a single argument, event
, containing details about the event:
event
(often abbreviated to evt
, ev
or e
) will be different for each event type. In this case, assuming the user is using a mouse and not a touchscreen, clicking anywhere on the page will return a
MouseEvent
which contains various data telling us where on the page the click landed, which mouse button was clicked, and so on.
Removing Event Listeners
When we create a button that will always remain active, it’s fine for an event listener to remain active until you close the page. However, it’s best practice to clean up event listeners when you are done with them. There’s a corresponding .removeEventListener
method, but for that to work, we need to use a named callback.
Now we can remove the event listener:
Prevent the Default Behavior of an Event
When we add the click
event to the button above, clicking on the button still works as normal, by which we mean any functionality that a browser usually associates with buttons will be processed.
Often, that’s not what we want. For example, we might want to use an <a>
element instead of a button (a common, if questionable, practice):
Here, we’ve styled the anchor element to look like a button using the .button
class (details omitted). In this case, we don’t want clicks on the link to exhibit normal link behavior such as redirecting to a new page.
To disable the default behavior, we’ll call
event.preventDefault
in the callback. Only our custom behavior will be processed.
The resize
Event
Another important event is the
resize
event, which occurs when the browser window changes size. This also occurs when a user rotates their phone from portrait to landscape mode.
Note that the resize
event won’t work when attached to anything other than the window
:
The Virtual Viewport
Mobile devices render your page in a virtual viewport, then scale it and draw it onto the physical device screen. These virtual pixels (CSS pixels) may not be the same size as the device’s physical pixels. They often differ by a factor of between two and five. This is known as the device’s pixel ratio.
The Device Pixel Ratio
We’ll need to take the pixel ratio into account when we set the size of three.js scenes to make sure rendered images are not blurry on mobile devices. We can access this value using
window.devicePixelRatio
:
Drawing Animation Frames
Usually, our three.js scene will contain movement. This means drawing frames - lots of frames! Ideally, we want to draw 60 frames per second. Modern browsers have a function designed to help us with this called
window.requestAnimationFrame
. Since requestAnimationFrame
is part of the DOM API, it’s not available in other environments such as Node.js.
We’ll cover how this works in a lot more detail in Ch 1.7 when we set up our animation loop.
That’s it for our exploration of the DOM and DOM API. We’ve touched only a fraction of the functionality added to JavaScript by the web browser here, but we’ve covered everything you need to follow the examples in this book. In this chapter and the last, we’ve covered a lot of syntax. In the next chapters, we’ll take a deeper look at the reality of using JavaScript to build applications. First up is a deep dive into JavaScript modules.