JavaScript Basics #3

Starting from this article, we are going to dive into the practical application of the JavaScript language in web development. We’ll talk about how JavaScript, HTML, and CSS can work together to make your web pages more appealing and interactive.

Exploring the DOM structure in JavaScript

Let’s start with a quick review of the document object model. Here is a simple HTML document:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">

<head>
  <title>Example Page</title>
</head>

<body>
  <h1>Example Page</h1>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  </p>
  <div>
    <p>Vestibulum fringilla lorem ac elit suscipit, nec suscipit nunc posuere.</p>
    <p>Proin efficitur eros scelerisque consequat <a href="https://www.ericsdevblog.com/">pulvinar</a>.</p>
  </div>
</body>

</html>

Each HTML element can be seen as a box. For example, the example document has the following structure:

HTML DOM

For each box, JavaScript automatically creates a corresponding object, which we can interact with to find out more details about that box, such as its content, attributes, etc. This kind of representation is referred to as the document object model, or DOM for short.

The second important feature of this DOM structure is that the boxes are all connected to each other, which means if we pick a starting point, it is possible for us to move to any other node on the page. For example, the <body> node has three child elements, <h1>, <p> and <div>. The <div> node has another two paragraphs (<p>) has child elements. So, to locate the paragraph with a link (<a>) in this example document, we can go from <html> to <body> to <div> and finally, locate the <p> node.

JavaScript and HTML

To import JavaScript code into an HTML document, we can use the <script> </script> tag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">

<head>
  <title>Example Page</title>
</head>

<body>
  ...

  <!--JavaScript-->
  <script>
        ...
  </script>

  <!--JavaScript in External File-->
  <script src="myScript.js"></script>

</body>

</html>

It is customary to put the JavaScript code before the end of the <body> tag. There are two ways to insert JavaScript, just like CSS, you can put it together with HTML, or you can put JavaScript in a separate file. For this tutorial, to make things easier, we’ll put HTML and JavaScript code together.

JavaScript treats each DOM box as an object, and that allows us to access any element in the HTML document using the global binding document. For example, document.body refers to the <body> element of the document.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
...
<body>
  ...
  <!--JavaScript-->
  <script>
    // Access body element
    let body_element = document.body;

    // Access h1 element
    let h1_element = document.body.firstElementChild;
    console.log(h1_element.tagName);

    // Access paragraph element (with link)
    let p_element = document.body.childNodes[5].lastElementChild;
    console.log(p_element.tagName);
  </script>
</body>

Go to Developer Tools -> Console in your browser, and you should see that the correct tag names have been returned.

Notice that the index number we use to locate the <div> element is 5, that is because the childNodes() method will return not only element nodes but also text nodes and comment nodes. For example, a paragraph element would have an element node <p>, and a text node, which is its content.

In web development, it is possible to reach any specific element in the document by starting at document.body and following a fixed path of properties. However, even though that’s possible, it’s still a bad idea, especially when you have a big HTML document with a complicated relationship tree. It is very easy to make a mistake. Luckily, JavaScript offers us some smarter ways of locating elements in an HTML document.

Locating HTML elements using JavaScript

We mentioned before that JavaScript treats all HTML elements as objects, which implies there are built-in methods for us to use. In fact, there are several different ways we can locate elements in an HTML file using JavaScript, and they actually work a lot like the selectors we talked about in our CSS course.

For instance, all HTML elements have a getElementsByTagName() method, which helps us locate elements with a specific tag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<body>
  <h1>Example Page</h1>
  <p class="paragraphs">Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  <div>
    <p class="paragraphs paragraphs_div">Vestibulum fringilla lorem ac elit suscipit, nec suscipit nunc posuere.</p>
    <p class="paragraphs paragraphs_div" id="paragraph_link">Proin efficitur eros scelerisque consequat <a
        href="https://www.ericsdevblog.com/">pulvinar</a>.</p>
  </div>

  <!--JavaScript-->
  <script>
    // Get all paragraph elements
    let p_elements = document.body.getElementsByTagName("p");
  </script>
</body>

This method will return a collection of elements with the specified tag, and you can access each one of them by specifying the index number, just like an array.

1
2
3
4
5
6
7
8
9
<!--JavaScript-->
<script>
  // Get all paragraph elements
  let p_elements = document.body.getElementsByTagName("p");

  // Get the second paragraph element and print its content
  let second_p = p_elements[1];
  console.log(second_p.innerHTML);
</script>

However, do not confuse this collection with an actual array, they are very similar, but not entirely the same. We cannot loop over it using for/of loop, we have to use the index numbers and run over the elements using a regular for loop. Or we can transform this collection into an array using Array.from method.

Once we’ve found the element we are looking for, we can access the attribute and content of that element using the dot (.) operator, and we can change their values as well:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!--JavaScript-->
<script>
  // Get all paragraph elements
  let p_elements = document.body.getElementsByTagName("p");

  // Get the second <p> element
  let second_p = p_elements[1];

  // Print its content
  console.log(second_p.innerHTML);

  // Change its content
  second_p.innerHTML = "Changed content.";

  // Print its attributes
  console.log(second_p.attributes);

  // Access one of the attributes
  console.log(second_p.getAttribute("class"));
</script>

The second method is document.getElementById(), it is used to find one single element, instead of returning a collection of elements. Note that this method does not exist under every element object, there is no document.body.getElementById().

1
2
3
4
5
6
<!--JavaScript-->
<script>
  // Get an element based on ID
  let element = document.getElementById("paragraphLink");
  console.log(element.innerHTML);
</script>

The third method is similar, it helps us locate elements with the same class name. That is getElementsByClassName(), which searches the entire document to find a collection of elements with the specified class name.

1
2
3
4
5
6
<!--JavaScript-->
<script>
  // Get an element based on class name
  let element = document.getElementByClass("text-red");
  console.log(element.innerHTML);
</script>

The last method is the combination of all of them, which is why it is the most commonly used method when we are trying to locate an element in HTML, it is the querySelector().

1
2
3
document.querySelector("div") // Matches a tag
document.querySelector("#myID") // Matches an ID
document.querySelector(".myclass") // Matches a class

Adding and deleting elements

Next, it’s time to talk about how to manipulate these HTML elements once we’ve located them. In fact, almost everything in the DOM structure can be changed.

For instance, we can remove an element like this:

1
2
3
// Get an element based on ID, and then remove it from the page
let element = document.getElementById("paragraphLink");
element.remove();

Or we can create a new element, and add it to the DOM structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Create new paragraph element
let new_p = document.createElement("p");

// Create content for the new <p> element
let new_text = document.createTextNode("This is a new paragraph.");

// Append the content to the <p> element node
new_p.appendChild(new_text);

// Add the new paragraph to the DOM structure
let element = document.getElementById("paragraphLink");
element.append(new_p);

As we mentioned before, a paragraph element should have a <p> element node, followed by a text node representing its content.

We can also replace one element with another:

1
2
3
// Replace a paragraph with the new paragraph
let element = document.getElementById("paragraph_link");
element.replaceWith(new_p);

In this SECTION, we briefly talked about how to locate and manipulate HTML elements using JavaScript. However, you may have noticed that all the changes are made instantly when we refresh our browser, which is not very interactive. In the next article, we are going to discuss what other events we can use to trigger JavaScript to perform an action.

Event handlers

In computer programming, an event is a user input, such as mouse and keyboard actions, and the program is usually expected to respond with something. This process is called event handling. Let’s first take a look at a very simple example. We have an HTML document with a paragraph, and we want the page to return a message when it is clicked.

1
2
3
4
5
6
7
<p>Click this document to activate the handler.</p>
<script>
  // Recall that the () => {} syntax is how we define an arrow function in JavaScript
  window.addEventListener("click", () => {
    console.log("You knocked?");
  });
</script>

This time, the output message will only appear in the console when you click on the document, instead of the moment the page is loaded.

Registering event handlers

The addEventListener() method is how we can register an event handler for the document node. In fact, we can use the same method to register event handlers for any node in the HTML document. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!--This time we register an event handler for the button but not the paragraph-->
<button>Click me</button>
<p>No handler here.</p>

<script>
  let button = document.querySelector("button");
  button.addEventListener("click", () => {
    console.log("Button clicked.");
  });
</script>

Actually, there is an onclick attribute for the HTML nodes which will have the exact same effect. However, you can only register one handler for each node that way. By using the addEventListener() method, we are able to register multiple handlers for each node.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<button>Click me</button>

<script>
  let button = document.querySelector("button");

  // When you click the button, the console outputs "Button clicked."
  button.addEventListener("click", () => {
    console.log("Button clicked.");
  });

  // When you click the button, the console outputs "Button double clicked."
  button.addEventListener("dblclick", () => {
    console.log("Button double clicked.");
  })
</script>

The removeEventListener() method, call with similar arguments can be used to remove an already registered event handler.

1
2
3
4
5
6
7
8
9
<button>Act-once button</button>
<script>
  let button = document.querySelector("button");
  function once() {
    console.log("Done.");
    button.removeEventListener("click", once);
  }
  button.addEventListener("click", once);
</script>

This button will only work once, after the removeEventListener("click", once) method is executed, the event handler registered for the button will be removed. The function that is passed to the removeEventListener has to be the same one that you passed to the addEventListener method.

Propagation

For most event types, the event handler registered for the node with children can receive events that happened in the children. For example, if a button inside a paragraph is clicked, the event handler registered for the paragraph will also be able to see that click event.

The event is said to propagate outward. For example, if both the button and the paragraph have an event handler, then the handler registered for the button will go first, then the paragraph, and it will keep propagating outward until it reaches the root of the document.

This feature can be quite useful sometimes, however, it is not always what we want. Luckily, we can stop the propagation using the stopPropagation() method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!--<button> is the child of <p>-->
<p>A paragraph with a <button>button</button>.</p>
<script>
  let para = document.querySelector("p");
  let button = document.querySelector("button");
  para.addEventListener("mousedown", () => {
    console.log("Handler for paragraph.");
  });
  button.addEventListener("mousedown", event => {
    console.log("Handler for button.");
    // If the button is clicked with the right mouse button, there will be no propagation
    if (event.button == 2) event.stopPropagation();
  });
</script>

Sometimes we want to register event handlers for multiple elements on the page. To do this we can use the target attribute to cast a wide net for a type of event.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<button>A</button>
<button>B</button>
<button>C</button>
<script>
  document.body.addEventListener("click", event => {
    if (event.target.nodeName == "BUTTON") {
      console.log("Clicked", event.target.textContent);
    }
  });
</script>

Default actions

A lot of the events have a default action, for example, when you click on a link, you will be taken to the link’s target, if you press the down arrow, the browser will scroll the page down. You can prevent that default action from being activated by using the preventDefault() method. Let’s try something completely useless but very interesting.

1
2
3
4
5
6
7
8
9
<a href="https://developer.mozilla.org/">MDN</a>
<script>
  let link = document.querySelector("a");
  // When you click the link, instead of going to the URL that link specifies, the console will just output "Nope."
  link.addEventListener("click", event => {
    console.log("Nope.");
    event.preventDefault();
  });
</script>

Even though this is possible, don’t do this unless you have a very good reason to, or it will be very confusing for the users.

Key events

Now we have discussed how event handlers work in general, it’s time to take a closer look at all the different types of events. The first one we are going to talk about is the key event.

When a key on your keyboard is pressed, it will trigger a keydown event, and when it is released, it triggers a keyup event.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<p>This page turns violet when you hold the V key.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == "v") {
      document.body.style.background = "violet";
    }
  });
  window.addEventListener("keyup", event => {
    if (event.key == "v") {
      document.body.style.background = "";
    }
  });
</script>

Looks very simple, however, you do need to be very careful about the keydown event. It is not a one-time thing, instead, it will keep being triggered over and over again, for as long as the key is being pressed, until it is released. You can experiment with the previous code, and see what happens when you keep the key pressed.

There are also some special keys like CTRL, ALT, and SHIFT. These are called modifier keys, they modify the original value of other keys by forming a key combination. For instance, when you press a key while holding the SHIFT key, "s" will become "S", "1" will become "!" etc. We can register event handlers for key combinations like this:

1
2
3
4
5
6
7
8
<p>Press Control-Space to continue.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == " " && event.ctrlKey) {
      console.log("Continuing!");
    }
  });
</script>

Pointer events

Pointer, as the name suggests, is used to point at things on the screen. There are primarily two ways that you can use to do that, either with a mouse or a touch screen, and they produce different types of events.

Mouse clicks

Mouse clicks work similarly to key events. When you press a mouse button, a mousedown event is triggered, and when you release that button, a mouseup event is triggered. And after the mouseup event, a complete click is finished, so a click event will be fired.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<button>Click me!</button>

<script>
  let button = document.querySelector("button");

  button.addEventListener("mousedown", event => {
    console.log("mouse down");
  });
  button.addEventListener("mouseup", event => {
    console.log("mouse up");
  });
  button.addEventListener("click", event => {
    console.log("button clicked");
  });
</script>

When two clicks happen very close together, a dblclick (double click) event will be triggered after the second click.

1
2
3
4
5
6
7
8
<button>Double click me!</button>

<script>
  let button = document.querySelector("button");
  button.addEventListener("dblclick", (event) => {
    console.log("double clicked");
  });
</script> 

Mouse motion

When a mouse pointer moves, a mousemove event is triggered.

1
2
3
4
5
6
7
8
<p>Move the cursor onto this paragraph to turn it red.</p>

<script>
  let para = document.querySelector("p");
  para.addEventListener("mousemove", (event) => {
    para.style.color = "red";
  });
</script>

This can be very useful when you are trying to implement some sort of drag and drop functionality. But to do that, we need to first track the location of the cursor. To get that information, we can either use the event’s clientX and clientY properties, which contain the event’s coordinates (in pixels) relative to the top-left corner of the window, or pageX and pageY, which are relative to the top-left corner of the whole document.

For example, the following script will output the coordinates of the click events that happened on the page.

1
2
3
4
5
6
7
8
<p>click anywhere</p>

<script>
  window.addEventListener("click", event => {
    console.log("X: " + event.clientX);
    console.log("Y: " + event.clientY);
  });
</script>

Here is a more complicated example, this program will display a bar, and you can drag it to change its width.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
  let lastX; // Tracks the last observed mouse X position
  let bar = document.querySelector("div");
  bar.addEventListener("mousedown", event => {
    if (event.button == 0) { // if the left button is being held
      lastX = event.clientX;
      // If the cursor moves while the left button is being held
      window.addEventListener("mousemove", moved);
      event.preventDefault(); // Prevent selection
    }
  });

  function moved(event) {
    // If no button is being held, remove the "mousemove" event handler
    if (event.buttons == 0) { // Notice this is "buttons" not "button"
      window.removeEventListener("mousemove", moved);
    } else {
      let dist = event.clientX - lastX;
      let newWidth = Math.max(10, bar.offsetWidth + dist);
      bar.style.width = newWidth + "px";
      lastX = event.clientX;
    }
  }
</script>

Notice that we used two different ways to access which button is pushed (The button property and the buttons property), and they clearly work differently. Their main difference is that the button property can only tell you which button (singular) is clicked, while the buttons property can tell you if a combination of buttons is pushed.

The button property

  • 0: Primary button pressed, usually the left button or the un-initialized state
  • 1: Auxiliary button pressed, usually the wheel button or the middle button (if present)
  • 2: Secondary button pressed, usually the right button
  • 3: Fourth button, typically the Browser Back button
  • 4: Fifth button, typically the Browser Forward button

The buttons property

  • 0: No button or un-initialized
  • 1: Primary button (usually the left button)
  • 2: Secondary button (usually the right button)
  • 4: Auxiliary button (usually the mouse wheel button or middle button)
  • 8: 4th button (typically the “Browser Back” button)
  • 16 : 5th button (typically the “Browser Forward” button)

The buttons property is able to record button combinations. When more than one button is pressed simultaneously, the values are combined. For example, when the primary and secondary buttons are pressed at the same time, the value will be 3.

Touch events

In most cases, the mouse events will also work when the user is using a touch screen. For example, when you are tapping a button on your screen, it will trigger a click event, it will be the same as clicking it with a mouse pointer.

However, this won’t work in some cases, such as the resizing bar example we talked about before. Because the touch screen doesn’t have multiple buttons, and it can’t track your finger’s position when you are not touching the screen. So to solve this problem, we have a few specific event types triggered only by touch interaction.

When your finger touches the screen, it triggers a touchstart event, when it moves while touching, it triggers a touchmove event, and finally, when you lift your finger, it triggers a touchend event.

Scroll events

A scroll event is triggered when you place the cursor on an element and scroll the middle button of your mouse. This can be very useful when you are trying to make your webpage more responsive. For example, when you go to the product showcasing page on Apple’s website, notice that the elements on the page will move as you scroll down.

Here is an example of a progress bar, it starts at 0% and will go to 100% as you scroll down.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<style>
  #progress {
    border-bottom: 20px solid orange;
    width: 0;
    position: fixed;
    top: 0; left: 0;
  }
</style>
<div id="progress"></div>
<script>
  // Create some content
  document.body.appendChild(document.createTextNode(
    "supercalifragilisticexpialidocious ".repeat(1000)));

  let bar = document.querySelector("#progress");
  window.addEventListener("scroll", () => {
    let max = document.body.scrollHeight - innerHeight;
    bar.style.width = `${(pageYOffset / max) * 100}%`;
  });
</script>

Focus events

When an element gains focus, a focus event will be triggered, and when the element loses focus, a blur event will be triggered. Unlike the other event types we’ve discussed, these two do not propagate.

This is most commonly used on HTML field elements. When you click on a text field and start typing some texts, that field is said to be in focus, and when you move on from that field and click on other elements, that field element loses focus.

This is an example that displays help texts for the text field that is currently in focus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Your age in years"></p>
<p id="help"></p>

<script>
  let help = document.querySelector("#help");
  let fields = document.querySelectorAll("input");
  for (let field of Array.from(fields)) {
    field.addEventListener("focus", event => {
      let text = event.target.getAttribute("data-help");
      help.textContent = text;
    });
    field.addEventListener("blur", event => {
      help.textContent = "";
    });
  }
</script>

Load events

The load event is triggered when the entire page finishes loading. This is different from directly putting the code inside the <script> tag directly without event handlers. The code inside the <script> tag is run immediately when the tag is encountered. This might be too soon in some cases.

There is also a similar event type called beforeunload. It is triggered when you close a page, the primary use of this event is to prevent the user from accidentally closing their unsaved work.


comments powered by Disqus