Discover three.js is now open source!
Word Count:3247, reading time: ~16minutes

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:

Using the console to view the value of a variable
    
let x = 'something';

// You have done some work and now you expect that x = 'something else'.
// But how do you test this? Simple! Use:

console.log(x);

// output: "something else"

  

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:

A minimalistic HTML document
    

<!DOCTYPE html>
<html>
  <head>
    <title>Page Title</title>
  </head>

  <body>
    <h1>A Heading</h1>
  </body>
</html>

  

…is represented as a series of nested JavaScript objects that looks like this:

The DOM: A representation of an HTML document in JavaScript
    
window = {
document: {
head: {
...
},

    body: {
      ...
    },

},
};

  

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:

Using the DOM API, we can access properties and methods such as style on HTML elements
    
window.document.head.style;

window.document.body.style;

  

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.

There’s rarely any need to type window
    
// These two statements are equivalent
window.document;
document;

  

This applies to any custom data to you add to window as well.

    
window.yourData = {
squirrels: 'nice'
};

// Now these statements are equivalent
window.yourData;
yourData;

  

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:

Our simple HTML document again
    

<!DOCTYPE html>
<html>
  <head>
    <title>Page Title</title>
  </head>

  <body>
    <h1>A Heading</h1>
  </body>
</html>

  

… we’ll find the <title> element in document.head.children and the <h1> element in document.body.children.

An elements children are can be accessed via element.children
    
console.log(document.head.children);
// => HTMLCollection(1) [title]

  

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:

Adding some more elements to our HTML document
    

<!DOCTYPE html>
<html>
  <head>
    <title>Title</title>
  </head>

  <body>
    <h1>Title</h1>

    <div id="scene-container"></div>

    <p class="alert">Oh No!</p>
    <p class="alert">Watch Out!</p>

  </body>
</html>

  

…we can access the elements individually using querySelector:

document.querySelector returns the first matching element
    
const html = document.querySelector('html');

const head = document.querySelector('head');

const title = document.querySelector('title');

const body = document.querySelector('body');

const heading = document.querySelector('h1');

const sceneContainer = document.querySelector('#scene-container');

// Return the first of the two alerts
const firstAlert = document.querySelector('.alert');

  

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.

.querySelectorAll returns all matching elements
    
const allAlertElements = document.querySelectorAll('.alert');

  

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.

Improve performance by avoiding searching through the entire document
    
const bodyElem = document.querySelector('body');
const headingElem = bodyElem.querySelector('h1');

// alternatively:
const headingElem = document.body.querySelector('h1');

  

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:

Changing the text in the page heading
    
const headingElem = document.querySelector('h1');

headElem.textContent = 'The Secret and Amazing Life of Honey Bees';

  

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 current size of the browser window
    
const windowWidth = window.innerWidth;
const windowHeight = 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.

Accessing the width and height of an element
    
const sceneContainerElem = document.querySelector('#scene-container');

const sceneWidth = sceneContainerElem.clientWidth;
const sceneHeight = sceneContainerElem.clientHeight;

  

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:

Creating a new sub-heading element
    
const headingElem = document.createElement('h2');

headingElem.textContent =
'Section Two: Waggle Dancing for Children and Teenagers';

  

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:

Append the new element to our page
    
document.body.append(headingElem);

  

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.

Listen for click events on the whole window
    
window.addEventListener('click', (event) => {
console.log('You clicked the mouse!');
} );

  

As another example, let’s say we have a button defined in HTML:

An ominous button
    
<button id="risky-click-button">Release the bees!</button>

  

We can listen for clicks on the button like this:

Listen for events on the button
    
const button = document.querySelector('#risky-click-button');

button.addEventListener('click', (event) => {
console.log('Buzz buzz!! Buzz buzz!!');
});

  

The event Argument

The callback function receives a single argument, event, containing details about the event:

The callback receives an object containing details about the event
    
window.addEventListener('click', (event) => {
console.log(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.

An event listener with a named callback
    
const releaseBees = event => {
console.log('Buzz buzz!! Buzz buzz!!');
};

button.addEventListener('click', releaseBees);

  

Now we can remove the event listener:

Using .removeEventListener to remove a listener
    
// on second thoughts, let's not release the bees...
button.removeEventListener('click', releaseBees);

  

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):

An anchor element masquerading as a button
    
<a class="button" id="risky-click-button" href="#">Release the bees!</a>

  

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.

.preventDefault prevents default event behavior
    
const fakeButton = document.querySelector('#risky-click-button');

fakeButton.addEventListener('click', (event) => {
event.preventDefault();

console.log('Buzz buzz!! Buzz buzz!!');
});

  

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.

Listening for the resize event
    
window.addEventListener('resize', () => {
console.log('The new width is:', window.innerWidth);
console.log('The new height is:', window.innerHeight);
});

  

Note that the resize event won’t work when attached to anything other than the window:

The resize event must be added to the entire window
    
// this won't work!
document.addEventListener('resize', onResizeCallback);

  

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:

The pixel ratio is the difference between physical pixels in the screen and CSS pixels
    
const pixelRatio = 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.

Import Style
Selected Texture