Functions Built-In to Your Browser: the Document Object Model 0.10

FUNCTIONS BUILT-IN TO YOUR BROWSER: THE DOCUMENT OBJECT MODEL

Your web browser comes with a lot of extra functions built that we can use in our JavaScript application, hundreds, in fact. We’ll only need to use a couple of them though. Let’s take a brief look at the ones we’ll be using now.

While working through this chapter, if you like, you can open up your Browser Console and try typing the commands in there to verify that they work as expected.

This chapter is the $4^{th}$ and final of our introductory tutorials. Feel free to skip this chapter for now and use it as a reference if you we use any browser functions that you’re not familiar with.

On the other hand, if you’re new to web development, you should take a few moments to at least skim this chapter.

The Document Object Model (DOM)

The Document Object Model decribes the way in which a HTML document is modelled as a JavaScript object.

Given a simplifed webpage that looks like this:

<!DOCTYPE html>
<html>

  <head>

  </head>

  <body>

  </body>

</html>

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

window = {

  document: {

    head: {

    },

    body: {

    },

  }

}

This is a highly simplified examples - there are lots of properties and methods attached to the window and document objects, as well as to every sub object. Let’s examine the parts of this that we will be using now.

The global object: Window

The top-level object in the DOM is called the window. In fact, every other object in a JavaScript application that’s running in a web browser gets attached to the window object, and, while we are running our JavaScript code in a web browser, we can access the window object at any time.

This top-level object is referred to as the global object, meaning that it contains the entire world of your application.

For example, if we forget to write const or let when creating a variable, it will get attached to the window object:

x = 5;

Since we didn’t use let or const, this is actually equivalent to:

window.x = 5;

Go ahead and open up your browser console and test it out!

Whenever we include the three.js script, it creates a keyword THREE that we can use to access all the features of three.js. For example, to create a spotlight, we will type:

const spotlight = new THREE.SpotLight();

The THREE object is also attached to the window object, so these two statements are also identical:

  1. window.THREE
  2. THREE

Whenever JavaScript compiler finds a word it doesn’t know, it automatically searches for it on the window object.

In practice, we’ll always just type THREE to keep things short, and only include window. when we want to make it clear in the code that we’re using a built-in browser method.

All of the rest of the functions that we’ll be using will be attached either to the window object or to the document object.

The name window is specific to browsers - other JavaScript environments will also have a global object, but it may be called something else. For example, in Node.js it’s called global (it also behaves a little differently).

Document

Next level down is the document object. This represents the actual HTML document that is loaded.

Since this attached to the window, we should actually be typing window.document to access this. However, since everything is attached to the window, JavaScript allows us to avoid typing it every time, meaning that the following are equivalent:

document.head and document.body

Inside the document, the document.head and document.body refer the <head> and <body> elements in your HTML document.

The Elements that Make Up Your Page: Nodes

We’ve been referring to the pieces that make up our HTML as elements, which is common practice. Technically though, they are known as nodes. This includes the <head>, <body>, <h1>, <title>, any <div> elements, and so on.

Accessing Nodes: document.querySelector

One of the most common needs in a JavaScript application is accessing and manipulating the HTML elements in the page, and, as you would expect, there are lots of functions for doing this. We’ll restrain ourselves to using just one selector function which is attached to the document object, called querySelector. Supposing we have HTML like this:

<body>

  <h1>Title</h1>

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

</body>

Then we can access the elements using querySelector like so:

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

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

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

Note that we’re using the # symbol to refer to an element by ID, just as we do in CSS.

If querySelector finds more than one matching node, it will just return the first node. If we want all matching nodes, then we can use querySelectorAll instead.

Node.textContent

Our headingElem just says Title. That’s a little generic, don’t you think? We can change the text inside elements in a number of ways, but we’ll be using node.textContent.

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

headElem.textContent = "The Secret and Amazing Life of Bumblebees"

Much better!

Getting the Window’s Dimensions: window.innerWidth and window.innerHeight

At any time, we can access the width and height of the browser windows using window.innerWidth and window.innerHeight:

const windowWidth = window.innerWidth;

const windowHeight = window.innerHeight;

Getting a Node’s Dimensions: node.clientWidth and node.clientHeight

Similarly, we can access the width and height of a node using node.clientWidth and node.clientHeight:

const headingWidth = headingElem.clientWidth;

const headingHeight = headingElem.clientHeight;

There are other ways of getting information about a nodes dimensions, such as node.offsetWidth which takes into account any borders and padding that may have been set in the CSS, and node.scrollWidth, which also includes parts of the element that may be offscreen. We’ll only use clientWidth in this book, however.

Creating Nodes with document.createElement

We can also create nodes directly:

const headingElem = document.createElement( 'h1' );

headElem.textContent = "Section Two: Waggle Dancing for Children and Teenagers"

Adding a Node as a Child of an Existing Node: node.appendChild

However, the node that we just created exists in limbo - well, in our computer’s memory. We’ll need to add it to our page to be able to see it. Here’s how to add it as a child of the <body> element.

document.body.appendChild( headingElem );

Listening for Events with eventTarget.addEventListener

We can use the addEventListener API to listen for changes. The eventTarget can be many different objects, such as:

And we can listen for all kinds of different events, such as clicks and keypresses from the user, or the browser’s window changing sizes, or the user scrolling, and so on.

The one that we’ll be using in every example past the third chapter is the resize event, which must be attached to the window.

const onResizeCallback = () {

  console.log( 'The new width is:', window.innerWidth );
  console.log( 'The new height is:', window.innerHeight );

}

window.addEventListener( 'resize', onResizeCallback );

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

document.addEventListener( 'resize', onResizeCallback ); // this won't work!

We can listen for clicks on a button like this:

<button id="click-me">Click Me!</button>
const button = document.querySelector( '#click-me' );

button.addEventListener( 'click', ( e ) => {

  console.log( 'You clicked the button!' );

} );

Note that we’re using an inline callback here, rather than defining it first, and that the callback gets passed a parameter which we’re calling e, for event.

This parameter will hold quite a bit of information related to the event and will be different for each event type.

We can listen for key presses with keydown and keyup:

window.addEventListener( 'keydown', ( e ) => {

  if ( e.key === 'ArrowUp' ) {

    console.log( 'You pressed the Up Arrow Key' );

    e.preventDefault();

  }

} );

window.addEventListener( 'keyup', ( e ) => {


  if ( e.key === 'ArrowUp' ) {

    console.log( 'You released the Up Arrow Key' );

  }

} );

Here, we’re calling the preventDefault() method, which is telling the browser not to follow through with its default behavior in the event of the up arrow key being pressed (normally it would scroll up).

We’ll listen for some other events over the course of this book, and we’ll explain those as we go.

window.devicePixelRatio

As we mentioned in the HTML and CSS Intro chapter, mobile devices render your webpage in a virtual viewport, and then afterward scale it and draw it onto the physical device screen. The virtual pixels (CSS pixels) may not be the same size as the devices physical pixels! In fact, they commonly differ by a factor of between two and five.

We’ll need to take this into account when rendering our scenes to make sure that things are not blurry. Fortunately, three.js does all the hard work for us, and we just need to make sure that we tell it what the ratio between the real and CSS pixels is, which we can access using window.devicePixelRatio:

const pixelRatio = window.devicePixelRatio;

window.requestAnimationFrame

window.requestAnimationFrame( callback ) is very important to us, as three.js developers, because it allows us to set up a simple and performant animation loop. We’ll cover this function in detail in Ch 1.2: Lights! Color! Actions

Debugging Your Code with Your Browser’s Developer Tools

Press CTRL + SHIFT + J, or CMD + Shift + J on a Mac, and take a look at the window that opens up. Looks familiar? Great. If not take a moment now to become familiar with it, especially the tab called Elements which holds data about your HTML structure and the CSS applied to it, and the tab called Console where any messages, warnings or errors related to your app will show up. You’ll be using this a lot throughout your career as a three.js developer - or indeed while doing any kind of web development.

The most basic requirement for getting a three.js scene running is to load the three.js script, so let’s take a few moments to see how we can use the browser console to test whether this has been done successfully.

First of all, open up the browsers console for this page. Type THREE (all capitals) into the console and press enter. You will see something like this (the exact message will vary between browsers):

Uncaught ReferenceError: THREE is not defined
    at <anonymous>1

OK, so three.js has not been loaded on this page.

Next, open up this page (it’s the empty template that we’ll be building on in Chapter 1.1).

Repeat the process by opening up the console and typing THREE. This time you should see something similar to this:

Object {
    WebGLRenderTargetCube: WebGLRenderTargetCube(),
    WebGLRenderTarget: WebGLRenderTarget(),
    WebGLRenderer: WebGLRenderer(),
    ShaderLib: Object,
    UniformsLib: Object,
    UniformsUtils: Object,
    ShaderChunk: Object,
    FogExp2: FogExp2(),
    Fog: Fog(),
    Scene: Scene(),
    367 more
}

Great! The empty template is all set up and ready to go.

Uncaught ReferenceError: SomeObject is not defined is a common error and means that you are referencing an object that does not exist (in the current scope). There can be many reasons for this, such as a script that hasn’t loaded correctly, or calling an object before it was defined, or somewhere that it is not accessible… or even just misspelling the object’s name! There’s nothing more annoying than spending an hour trying to track down a bug only to find that you had typed Someobject instead of SomeObject - so always double check your spelling first!

Logging to the Console with console.log

The main debugging technique we’ll be using in this book is logging information to the console using console.log(). It’s a simple but powerful technique, and until you start to create more complex apps it’s likely to be the only one that you need.

Here’s how to check the value of a variable called x using the browser console:

let x = 'something';

// Do some stuff with x, so that now you expect that x = 'something else'.
// But how do you test this? Simple! Just use:

console.log( x );

// output: "something else"

There’s a range of other console methods that we can use, such as console.warn, console.error. For our methods, console.log will be enough, but you it’s worth taking a few moments to familiarise yourself with all the console methods. console.table in particular can be very handy, while console.time is very useful for testing how long things like loading models takes.