JS 103 - The DOM and Events

"How do you comfort a JavaScript bug? You console it"

What is a Document Object Model?

The Document Object Model (usually just referred to as the DOM) is a representation of the document (a.k.a a web page) that can be changed by scripts and programming languages. It is a shared current state of the web page.

The DOM is presented in the form of nodes. Every element in HTML becomes a node in the DOM. Javascript sees HTML as a collection of nodes all related to each other. Go ahead and see for yourself - right click on any element on a page and select inspect. Then open the console and type,

$0 instanceof Node

`$0` is a handy shortcut in your javascript console. It refers to the object currently being inspected. Note that you can only use this while debugging in a console, not in a js file. `instanceof` checks to see if an object is part of a certain type of objects. In this case we can see that any element on the page is an instance of a `Node`.

These nodes and objects have structure, style, and content that can be changed via javascript. This is how we make pages dynamic.

Where do I find the DOM?

The current state of the DOM can be found using the inspect tool. With the inspect tool you can see the full DOM represented in HTML (highlighted below in red). With the inspect tool you can see and select the individual nodes and objects.

This looks a lot like a compiled version of our pug code, but there is a big difference. While pug is used to define the initial state of the DOM, once we start changing the DOM with javascript the "pug version" is no longer up to date. The pug also does not fully illustrate the methods and properties that the nodes have.

Ok, cool... but what can I do with the DOM?

Well, apart from making the intial web page, the DOM can be used to make the page more interactive by having it dynamically change. We'll break down exactly what you can dynamically do in to selection, attributes, methods and events. To keep things simple we'll use the console as it allows us to change the DOM in browser.

Selection

Whenever we want to select an element on the page we have to define a starting point to search from. Generally, we want to search from the very top of the tree - the main element that contains every other element on the page. This is always pre-defined as `document` in javascript.

console.log(document)

By logging document, we can see that it contains the root html element, as well as our header and body elements - each of these contains all of the html of the page, parsed into a DOM format.

Javascript selection methods

Javascript allows multiple ways of searching from a node. You can either search through that node's descendants or through that node's ancestors. For descendants we have multiple avenues of selection available to us.

Search direction Function names
Searches descendants
  • `.getElementById`
  • `.querySelector`
  • `.querySelectorAll`
  • `.getElementsByTagName`
  • `.getElementsByClassName`
  • `.children`
Searches ancestors
  • `.closest`
  • `.parentElement`

How to use each of these methods is pretty easy. Note that in the below examples, `document` can be replaced with any node.

Method Description Example Return
getElementById Allows you to select a single element with the given ID. Returns null if no elements were found.
document.getElementById("myList")
An HTMLElement containing the following:
<ul id="myList">
  <li id="first" class="listItem">uno</li>
  <li class="listItem">dos</li>
  <li class="listItem">tres</li>
</ul>
getElementsByClassName Allows you to select all elements with a given class. Returns an empty HTMLCollection if no elements were found.
document.getElementsByClassName("listItem")
An HTMLCollection containing the following:
[li#first.listItem, li.listItem,
li.listItem]
querySelector Allows you to select the first element that matches the given query.
document.querySelector("#first.listItem")
An HTMLElement containing the following:
<li class="listItem" id="first">uno</li>
querySelectorAll Selects all the elements that match the given query.
document.querySelectorAll("li")
An HTMLCollection containing the following:
[li#first.listItem, li.listItem,
li.listItem]
children Returns an array of the element's direct children
document.body.children
An HTMLCollection containing the following:
[div#header, div#content, div#footer]
closest Selects the first ancestor of an element that matches a query string.
randomDiv.closest('#content')
An HTMLElement
parentElement Returns the element's direct parent
randomDiv.parentElement
An HTMLElement

Exercises

If we visit this page we can see the following HTML.

<html>
  <head>
    <link rel="stylesheet" href="/prototypingCourse/styles/prism.css">
  </head>
  <body>
    <ul id="myList">
      <li id="first" class="listItem">uno</li>
      <li class="listItem">dos</li>
      <li class="listItem">tres</li>
    </ul>
    <input id="textInput" class="formType" type="textbox" value="I'm a value"></input>
  </body>
</html>

Use your knowledge of selectors to write the relevant javascript to select the following:

Attributes

Now that we've selected an item, what can we do with it? There is a big range of properties and methods that we can view and/or call on an element. We'll start by looking at getting and setting some attributes of some of the different elements in the previous DOM.

There is also a style attribute. Getting this attribute returns a `CSSStyleDeclaration`. The style attribute can be used to set the style of an element directly as below.

document.querySelector("li#first").style = "background-color: red"

If your styling needs to be dynamically set (e.g. it requires calculation) use the style attribute. However, if you are setting style statically, it is better practice to have a set class styling that you wish to apply and changing the class of an element. This maintains all styling in one location, makes it easier to debug, and prevents accidentally overriding other styling. It also makes it easier to potentially change the style applied if need be.

Attribute Description Example Return
id
className
classList
Returns the value of the items id and className respectively.
document.querySelector("ul").id
document.querySelector("input").className
document.querySelector("input").classList
"myList"
"formType"
["formType"] <- as a DOMTokenList
id = "new id"
className = "new classname"
Sets the value of the items id and className respectively.
document.querySelector("ul").id = "myNewList"
document.querySelector("input").className = "myNewClass"
The ul element's id is now "myNewList"
The input element's class is now "myNewClass"
innerHTML Returns the innerHTML of the item in string form.
document.querySelector("li#first").innerHTML
"uno"
innerHTML = "string of html" Sets the innerHTML of the item to the given string parsed as HTML
document.querySelector("li#first").innerHTML = "<img src='try.jpg' />"
The innerHTML of the li element with an id of "first" has been set to
<img src='try.jpg' />
value Returns the value of the item in string form.
document.querySelector("input").value
"I'm a value!"
value = "new value" Sets the value of the item to the given string
document.querySelector("input").value = "new value"
The value of the textbox is now "new value"
length Gets the number of nodes within the selected NodeList.
document.querySelectorAll("#myList > li").length
3

Methods

In comparison to attributes, methods are like functions that are owned by the element.

Method Description Example Return
add("className") appends the given class when called on the classList
document.querySelector("#first").classList.add("purple")
#first's class is now "listItem purple"
remove("className") removes the given class when called on the classList
document.querySelector("#first").classList.remove("purple")
#first's class is now "listItem"
toggle("className") adds the given class if it was not already in the classList otherwise it is removed
document.querySelector("#first").classList.toggle("purple")
#first's class is now "listItem purple"
getElementsByClassName
querySelector
querySelectorAll
These functions work just as they do when called on document, except they are scoped to only the nodes that are within the element they are called on.
document.querySelector("#myList").getElementsByClassName("listItem")
An HTMLCollection containing the following:
[li#first.listItem, li.listItem,
li.listItem]

Exercises

Let's revisit this page, but this time change some things around.

<html>
  <head>
    <link rel="stylesheet" href="/prototypingCourse/styles/prism.css">
  </head>
  <body>
    <ul id="myList">
      <li id="first" class="listItem">uno</li>
      <li class="listItem">dos</li>
      <li class="listItem">tres</li>
    </ul>
    <input id="textInput" class="formType" type="textbox" value="I'm a value"></input>
  </body>
</html>

Use your knowledge of node methods to make the following changes. Each change should be triggered by a single line of javascript.

DOM element creation

A big part of methods is DOM creation. This is where new elements are created dynamically. This can be used in various situations like adding and deleting a new row from a table.

The first method is createElement(). This can only be called on the document. It takes the type of element to create in string form and returns the new element. Remember this in itself does NOT add it to the webpage.

var newListItem = document.createElement("li")

We can give this new element attributes just as before. For example

newListItem.id = "new"
newListItem.className = "listItem"
newListItem.innerHTML = "quatro"

Now we can use appendChild to actually add it to the page.

document.getElementById("myList").appendChild(newListItem)

This results in the updated DOM looking like below:

<html>
  <head>
    <link rel="stylesheet" href="/prototypingCourse/styles/prism.css">
  </head>
  <body>
    <ul id="myList">
      <li id="first" class="listItem">uno</li>
      <li class="listItem">dos</li>
      <li class="listItem">tres</li>
      <li id="new" class="listItem">quatro</li>
    </ul>
    <input id="textInput" class="formType" type="textbox" value="I'm a value"></input>
  </body>
</html>

DOM element removal

Using removeChild() we can remove the child again using either of the two commands below.

document.getElementById("myList").removeChild(newListItem)
document.getElementById("myList").removeChild(document.getElementById("new"))

Both of these will return the DOM back to its original state. Any element can be removed (not just ones newly added) in this way. The removeChild method must however be called on the element's direct parent.

Events

Interactions with the DOM can also trigger events that can be reacted to. Replacing the dogs' stick with a more complicated toy, imagine an event as the dog biting the toy. The toy can squeak in response. The toy however does not squeak on its own it waits and "listens" for the bite "event".

To tune in to these events we need to use the method addEventListener(). This takes two arguments, the event it is listening for and the function to call when the event has been "heard". A full list of events that can be heard can be found here. Note that we do not use the "on" prefix for the events.

Examples

#box
#box
  background-color: red
  width: 100px
  height: 100px
.clicked
  border: 5px solid #4c9aff
var box = document.getElementById("box")
box.addEventListener("click", function(){
  box.classList.toggle("clicked")
})
#box2
#box2
  background-color: red
  width: 100px
  height: 100px
.clicked
  border: 5px solid #4c9aff
var box2 = document.getElementById("box2")
box2.addEventListener("keydown", function(){
  box2.classList.toggle("clicked")
})
#box3
#box3
  background-color: red
  width: 100px
  height: 100px
.clicked
  border: 5px solid #4c9aff
var box3 = document.getElementById("box3")
box3.addEventListener("mousedown", function(){
  box3.innerHTML += "ha "
})
#box4
#box4
  background-color: red
  width: 100px
  height: 100px
.clicked
  border: 5px solid #4c9aff
var box4 = document.getElementById("box4")
box4.addEventListener("mouseleave", function(){
  box4.addEventListener("mouseenter", function() {
    box4.innerHTML += "that was fun! "
  })
})

Remember to be careful when adding event listeners to avoid adding duplicate event listeners!

Removing an event listener requires both the event name and the function that was intially used.

#box5
#box6
#box5, #box6
  background-color: red
  width: 100px
  height: 100px
.clicked
  border: 5px solid #4c9aff
var box5 = document.getElementById("box5")
var box6 = document.getElementById("box6")
var toggleBorder = function() {
  box5.classList.toggle("clicked")
}
var stopClick = function() {
  box5.removeEventListener("click", toggleBorder)
  box6.removeEventListener("dblclick", stopClick)
  box6.addEventListener("dblclick", startClick)
}
var startClick = function() {
  box5.addEventListener("click", toggleBorder)
  box6.removeEventListener("dblclick", startClick)
  box6.addEventListener("dblclick", stopClick)
}
box5.addEventListener("click", toggleBorder)
box6.addEventListener("dblclick", stopClick)
#box7
#box7
  background-color: red
  width: 100px
  height: 100px
.clicked
  border: 5px solid green
var box7 = document.getElementById("box7")
var click = 0
box7.addEventListener("click", function clicker() {
  click ++
  console.log("ouch :(")
  if (click >= 5) {
    console.log("I've had enough!")
    box7.removeEventListener("click", clicker)
  }
})

Exercises

    • Set up a new prototype using the prototyping server template.
    • Create a #box element in the index.pug and give it some text. In the style.styl give it a width, height and a background color.
    • View the page in your browser of choice and open up the console. Using javascript change the text inside of the text.
    • Now we want to do the same with a button and without the console. Create a new file script.js, this will contain your javascript.
    • At the bottom of your index.pug include the javascript file using the below. Any included script files will get run from top to bottom on page load.
      script(src="script.js" type="text/javascript")
    • In your index file create a new button element. Give the button an id. In your javascript file listen for a click event on the button.
    • In the function that gets called on the click event you can use what you used in the console to change the innerHTML.
    • If you page looks something like this then you're good!

    link(href="style.styl" rel="stylesheet")
    
    #box hello there :D
    button#clicker Click me!
    
    script(src="script.js" type="text/javascript")
    document.getElementById('clicker').addEventListener("click", function() {
      document.getElementById('box').innerHTML = "how you doin?"
    })
    #box
      width: 100px
      height: 100px
      background-color: yellow
    
    • Set up a new prototype using the prototyping server template.
    • In the index.pug create an input(type="text") tag, with an id and placeholder text (hint there is a placeholder attribute for this)
    • In the prototype.js file's init function, set a "input" event listener on the input tag so that whenever the input is changed an event will be raised.

      The prototype.js init function gets run once the DOM content has loaded in the document. This is a useful function for anything that needs to be run at page load but only once.

    • Use the new value of the input to do something cool with it on the page. You can try and do this or use it as inpsiration for something better :D.

    The below answers are for the recreation of the example page.

    link(href="style.styl" rel="stylesheet")
    
    input#text(type="text" placeholder="Input your text here")
    .center
      #message I love _________ !
    
    script(src="script.js" type="text/javascript")
    document.getElementById("text").addEventListener("input", function() {
      document.getElementById("message").innerHTML = "I love " + document.getElementById("text").value + "!"
    })
    .center
      display: flex
      justify-content: center
      align-items: center
      width: 98vw
      height: 85vh
      font-size: 60px
      font-family: sans-serif
    
    input
      font-size: 20px
  1. We are creating a form but need to make sure that people under the age of 18 are not able to submit the form. We will do this in two ways. For both of these methods we'll be using these files.
    For the first method we will disable the submit button once a user tries to submit as an under 18 year old.

    Value

    When taking the value of an input[type="number"] you'll notice the numbers are returned in a string e.g. "8". To get the value as a number straight away use valueAsNumber rather than value. The other option is parseInt() a javascript function to parse a string into an integer. NaN stands for Not a Number. To check whether a variable is a NaN you will need isNan(). x == NaN will always return false even if x = NaN (you can test this with Boolean() but even NaN == NaN returns false).

    • In the empty script.js file add an event listener on a click on the submit button.
    • If the submit button has been clicked and the age is less than 18, remove the event listener on the submit button and add the disabled class to it.
    • If the age is above 18, use the console to log a "This form has been submitted" message.
    • If you are feeling fancy, you can add some extra checks before allowing a form to be submitted (e.g. check whether a name has actually been inputted)
    document.getElementById("submit").addEventListener("click", function checkForm() {
      var age = document.getElementById("age").valueAsNumber
      var name = document.getElementById("name").value
      if (isNaN(age) || name == "") {
        console.log("Please fill out the form completely before submitting.")
      } else if (age < 18) {
        console.log("You are not old enough to submit this form.")
        document.getElementById("submit").removeEventListener("click", checkForm)
        document.getElementById("submit").classList.add("disabled")
      } else {
        console.log("The form has been submitted.")
      }
    })
    • For the second method redownload the zip file and unzip it into a new folder. In the index.pug add the .disabled class to the submit button.
    • In this second method we want to only enable the submit button once an over 18 age has been entered. Create a "input" event listener on the age input. This will get fired anytime there is a change in the input of age.
    • When this event has been fired check if the age is appropriate then add a click event listener on the submit button which links to a submitForm function. Also remove the disabled class.
    • If the age is inappropriate remove the click event listner on the submit button and add the disabled class.
    • In the submit form function you can check whether name has been inputted or just print out "The form has been submitted" to the console.
    • var submitForm = function() {
        if (document.getElementById("name").value == "") {
          console.log("Please input a name before submitting.")
        } else {
          console.log("This form has been submitted.")
        }
      }
      
      document.getElementById("age").addEventListener("input", function() {
        var age = document.getElementById("age").value
        if (age >= 18) {
          document.getElementById("submit").addEventListener("click", submitForm)
          document.getElementById("submit").classList.remove("disabled")
        } else {
          document.getElementById("submit").removeEventListener("click", submitForm)
          document.getElementById("submit").classList.add("disabled")
        }
      })
  2. Have a look at this page and use javascript to try and remake it.
    link(href='style.styl' rel='stylesheet')
    
    #container.start
      #block Try and click me!
    
    script(src='script.js' type='text/javascript')
    body
      margin: 10vh 10vw
    
    #container
      display: flex
      width: 80vw
      height: 80vh
      &.start
        #block
          background-color: rgba(200, 200, 200, 0.5)
          box-shadow: 0px 0px 2px 2px rgba(60, 60, 60, 0.5)
      &.second
        justify-content: flex-end
        #block
          background-color: rgba(255, 60, 60, 0.2)
          box-shadow: 0px 0px 3px 3px rgba(255, 60, 60, 0.5)
      &.third
        align-items: flex-end
        justify-content: flex-end
        #block
          background-color: rgba(255, 60, 60, 0.5)
          box-shadow: 0px 0px 7px 7px rgba(255, 30, 30, 0.7)
      &.fourth
        justify-content: flex-start
        align-items: flex-end
        #block
          background-color: rgba(60, 60, 255, 0.1)
          box-shadow: 0px 0px 10px 2px rgba(30, 30, 255, 0.5)
    
    #block
      cursor: pointer
      text-align: center
      width: 300px
      height: 100px
      line-height: 100px
    
    var page = {
      block: document.getElementById("block"),
      container: document.getElementById("container"),
    
        toSecond: function() {
        page.container.classList.add("second")
        page.container.classList.remove("start")
        page.block.innerHTML = "Oh no you didn't!"
        page.block.removeEventListener("click", page.toSecond)
        page.block.addEventListener("click", page.toThird)
      },
    
        toThird: function() {
        page.container.classList.remove("second")
        page.container.classList.add("third")
        page.block.innerHTML = "Right, now you've done it!!!"
        page.block.removeEventListener("click", page.toThird)
        page.block.addEventListener("click", page.toFourth)
      },
    
        toFourth: function() {
        page.container.classList.remove("third")
        page.container.classList.add("fourth")
        page.block.innerHTML = "Breathe in... Breathe out..."
        page.block.removeEventListener("click", page.toFourth)
        page.block.addEventListener("click", page.toStart)
      },
    
        toStart: function() {
        page.container.classList.remove("fourth")
        page.container.classList.add("start")
        page.block.innerHTML = "Try and click me!"
        page.block.removeEventListener("click", page.toStart)
        page.block.addEventListener("click", page.toSecond)
      }
    }
    
    page.block.addEventListener("click", page.toSecond)