Lesson 4: Working with Browser Events

Lesson Overview

To recap, we'll be looking at :

  • Events - what they are

  • Responding to an event - how to listen for an event and respond when one happens

  • Event Data - harness the data that is included with an event

  • Stopping an event - preventing an event from triggering multiple responses

  • Event Lifecycle - the lifecycle stages of an event

  • DOM Readiness - events to know when the DOM itself is ready to be interacted with

Seeing An Event(Debug only)

如何開眼?

Chrome browser has a special monitorEvents() function that will let us see different events as they are occurring.

// start displaying all events on the document object
monitorEvents(document);

// turn off the displaying of all events on the document object.
unmonitorEvents(document);

Check out the documentation on the Chrome DevTools site: monitorEvents documentation

可以看到當你在browser中做這些事情的時候,都會有event產生:

  • click

  • dblclick

  • scroll

  • resize

Respond to Events

EventTarget Interface

The EventTarget page says that EventTarget:

is an interface implemented by objects that can receive events and may have listeners for them.

and

Element, document, and window are the most common event targets, but other objects can be event targets too…

If you take a look at the EventTarget Interface, you'll notice that it doesn't have any properties and only three methods! These methods are:

  • .addEventListener()

  • .removeEventListener()

  • .dispatchEvent()

1. Adding An Event Listener

Let's use some pseudo-code to explain how to set an event listener:

<event-target>.addEventListener(<event-to-listen-for>, <function-to-run-when-an-event-happens>);

So an event listener needs three things:

  1. an event target - this is called the target

  2. the type of event to listen for - this is called the type

  3. a function to run when the event occurs - this is called the listener

The <event-target> (i.e. the target) goes right back to what we just looked at: everything on the web is an event target (e.g. the document object, a <p> element, etc.).

The <event-to-listen-for> (i.e. the type) is the event we want to respond to. It could be a click, a double click, the pressing of a key on the keyboard, the scrolling of the mouse wheel, the submitting of a form...the list goes on!

The <function-to-run-when-an-event-happens> (i.e. the listener) is a function to run when the event actually occurs.

Let's transform the pseudo-code to a real example of an event listener:

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

mainHeading.addEventListener('click', function () {
  console.log('The heading was clicked!');
});

Check out the documentation for more info: addEventListener docs

2. Add Event Listener to the Project

在index file closing body tag前加入你的js script:

js file的內容就是之前console裡面寫的code。

To see a full list of all of the possible events you can listen for, check out the Events documentation: list of events

Remove an Event Listener

Let's say you only want to listen for just the first click event, respond to it, and ignore all other click events. The .addEventListener() event will listen for and respond to all click events.

To remove an event listener, we use the .removeEventListener() method. It sounds straightforward enough, right? However, before we look at .removeEventListener(), we need to take a brief review of object equality.

Are Objects Equal in JavaScript

Equality is a common task in most programming languages, but in JavaScript, it can be a little bit tricky because JavaScript does this thing called type coercion where it will try to convert the items being compared into the same type. (e.g. string, number,). JavaScript has the double equality (==) operator that will allow type coercion. It also has the triple equality (===) symbol that will prevent type coercion when comparing.

忽然發現js定義object field的方式不太一樣:

var a = {
    myFunction: function quiz() { console.log('hi'); }
};

Ok, so why do we care about any of this object/function equality? The reason is that the .removeEventListener() method requires you to pass the same exact listener function to it as the one you passed to .addEventListener().

<event-target>.removeEventListener(<event-to-listen-for>, <function-to-remove>);

Remember, the listener function must be the exact same function as the one used in the .addEventListener() call...not just an identical looking function.

This code will successfully add and then remove an event listener:

function myEventListeningFunction() {
    console.log('howdy');
}

// adds a listener for clicks, to run the `myEventListeningFunction` function
document.addEventListener('click', myEventListeningFunction);

// immediately removes the click listener that should run the `myEventListeningFunction` function
document.removeEventListener('click', myEventListeningFunction);

Code that does not work

Chrome上的Event listener可以用來查看目前選擇的element上有哪些listener:

找到function所在的js位置:

Phases of an Event

1. Event Phases

There are three different phases during the lifecycle of an event. They are:

  • the capturing phase

  • the at target phase

  • and the bubbling phase

And they actually follow the order above; first, it's capturing, then at target, and then the bubbling phase.

Most event handlers run during the at target phase, such as when you attach a click event handler to the button. The event arrives at the button (its target), and there's only a handler for it right there, so the event handler gets run.

But sometimes you have a collection of items -- such as a list -- and want to have one handler cover every item (and still have the option of individual handlers for some items.) By default, if you click on a child item and a handler doesn't intercept the click, the event will "bubble" upward to the parent, and keep bubbling until something handles it or it hits the document.

Capturing, on the other hand, lets the parent intercept an event before it reaches a child.

為什麼要有phases? 為了讓parent element可以在event到child之前或之後listen same event而後做動作。

要如何在listener中指定phase?

Up until this point, we've only seen the .addEventListener() method called with two arguments, the:

  • event type

  • and the listener

There's actually a third argument to the .addEventListener() method; the useCapture argument. From it's name, you'd think that if this argument were left out, .addEventListener() would default to using the capturing phase. This is an incorrect assumption! By default, when .addEventListener() is called with only two arguments, the method defaults to using the bubbling phase.

However, in this code, .addEventListener() is called with three arguments with the third argument being true (meaning it should invoke the listener earlier, during the capturing phase!).

document.addEventListener('click', function () {
   console.log('The document was clicked');
}, true);

2. The Event Object

Now that you know that event listeners fire in a specific order and how to interpret and control that order, it's time to shift focus to the details of the event itself.

取得event object資訊!

When an event occurs, the browser includes an event object. This is just a regular JavaScript object that includes a ton of information about the event itself. According to MDN, the .addEventListener()'s listener function receives:

a notification (an object that implements the Event interface) when an event of the specified type occurs

Up until this point, I've been writing all of the listener functions without any parameter to store this event object. Let's add a parameter so we can store this important information:

document.addEventListener('click', function (event) {  // ← the `event` parameter is new!
   console.log(event);
});

Notice the new event parameter that's been added to the listener function. Now when the listener function is called, it is able to store the event data that's passed to it!

3. The Default Action

As we just looked at, the event object stores a lot of information, and we can use this data to do all sorts of things. However, one incredibly common reason that professionals use the event object for, is to prevent the default action from happening.

Think about an anchor link on a webpage. There are probably a couple dozen links on this page! What if you wanted to run some code and display some output when you click on one of these links. If you click on the link, it will automatically navigate you to the location listed in its href attribute: that's what it does by default.

What about a form element? When you submit a form, by default, it will send the data to the location in its action attribute. What if we wanted to validate the data before sending it, though?

Without the event object, we're stuck with the default actions. However, the event object has a .preventDefault() method on it that a handler can call to prevent the default action from occurring!

const links = document.querySelectorAll('a');
const thirdLink = links[2];

thirdLink.addEventListener('click', function (event) {
    event.preventDefault();
    console.log("Look, ma! We didn't navigate to a new page!");
});

Avoid Too Many Events

以下的code created 200 event listener and 200 functions

const myCustomDiv = document.createElement('div');

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    newElement.addEventListener('click', function respondToTheClick(evt) {
        console.log('A paragraph was clicked.');
    });

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

Refactoring The Number of Event Listeners

如何優化?

第一個優化:大家共用同一個function(少了199個function)

const myCustomDiv = document.createElement('div');

function respondToTheClick() {
    console.log('A paragraph was clicked.');
}

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    newElement.addEventListener('click', respondToTheClick);

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

第二個優化:大家共用同一個event listener(少了199個event listener)

const myCustomDiv = document.createElement('div');

function respondToTheClick() {
    console.log('A paragraph was clicked.');
}

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    myCustomDiv.appendChild(newElement);
}

myCustomDiv.addEventListener('click', respondToTheClick);

document.body.appendChild(myCustomDiv);

Now the browser doesn't have to store in memory two hundred different event listeners and two hundred different listener functions. That's great for performance!

但是第二個優化有缺點:

However, if you test the code above, you'll notice that we've lost access to the individual paragraphs. There's no way for us to target a specific paragraph element. So how do we combine this efficient code with the access to the individual paragraph items that we did before?

We use a process called event delegation.

Event Delegation

Remember the event object that we looked at in the previous section? That's our ticket to getting back the original functionality!

The event object has a .target property. This property references the target of the event. Remember the capturing, at target, and bubbling phases?...these are coming back into play here, too!

Let's say that you click on a paragraph element. Here's roughly the process that happens:

  1. a paragraph element is clicked

  2. the event goes through the capturing phase

  3. it reaches the target

  4. it switches to the bubbling phase and starts going up the DOM tree

  5. when it hits the <div> element, it runs the listener function

  6. inside the listener function, event.target is the element that was clicked

So event.target gives us direct access to the paragraph element that was clicked. Because we have access to the element directly, we can access its .textContent, modify its styles, update the classes it has - we can do anything we want to it!

const myCustomDiv = document.createElement('div');

function respondToTheClick(evt) {
    console.log('A paragraph was clicked: ' + evt.target.textContent);
}

for (let i = 1; i <= 200; i++) {
    const newElement = document.createElement('p');
    newElement.textContent = 'This is paragraph number ' + i;

    myCustomDiv.appendChild(newElement);
}

document.body.appendChild(myCustomDiv);

myCustomDiv.addEventListener('click', respondToTheClick);

所以為了要access其中一個element,就是利用event object在bubbling phase中取得event target的資訊來做操作。

1. Checking the Node Type in Event Delegation

回傳的event要記得加上type checking

In the code snippet we used above, we added the event listener directly to the <div> element. The listener function logs a message saying that a paragraph element was clicked (and then the text of the target element). This works perfectly! However, there is nothing to ensure that it was actually a <p> tag that was clicked before running that message. In this snippet, the <p> tags were direct children of the <div> element, but what happens if we had the following HTML:

<article id="content">
  <p>Brownie lollipop <span>carrot cake</span> gummies lemon drops sweet roll dessert tiramisu. Pudding muffin <span>cotton candy</span> croissant fruitcake tootsie roll. Jelly jujubes brownie. Marshmallow jujubes topping sugar plum jelly jujubes chocolate.</p>

  <p>Tart bonbon soufflé gummi bears. Donut marshmallow <span>gingerbread cupcake</span> macaroon jujubes muffin. Soufflé candy caramels tootsie roll powder sweet roll brownie <span>apple pie</span> gummies. Fruitcake danish chocolate tootsie roll macaroon.</p>
</article>

In this filler text, notice that there are some <span> tags. If we want to listen to the <article> for a click on a <span>, you might think that this would work:

document.querySelector('#content').addEventListener('click', function (evt) {
    console.log('A span was clicked with text ' + evt.target.textContent);
});

This will work, but there's a major flaw. The listener function will still fire when either one of the paragraph elements is clicked, too! In other words, this listener function is not verifying that the target of the event is actually a <span> element. Let's add in this check:

document.querySelector('#content').addEventListener('click', function (evt) {
    if (evt.target.nodeName === 'SPAN') {  // ← verifies target is desired element
        console.log('A span was clicked with text ' + evt.target.textContent);
    }
});

Remember that every element inherits properties from the Node Interface. One of the properties of the Node Interface that is inherited is .nodeName. We can use this property to verify that the target element is actually the element we're looking for. When a <span> element is clicked, it will have a .nodeName property of "SPAN", so the check will pass and the message will be logged. However, if a <p> element is clicked, it will have a .nodeName property of "P", so the check will fail and the message will not be logged.

The .nodeName property will return a capital string, not a lowercase one. So when you perform your check make sure to either:

(1) check for capital letters

(2) convert the .nodeName to lowercase

// check using capital letters
if (evt.target.nodeName === 'SPAN') {
    console.log('A span was clicked with text ' + evt.target.textContent);
}

> // convert nodeName to lowercase
if (evt.target.nodeName.toLowerCase() === 'span') {
    console.log('A span was clicked with text ' + evt.target.textContent);
}

Recap

In this section, we looked at Event Delegation. Event Delegation is the process of delegating to a parent element the ability to manage events for child elements. We were able to do this by making use of:

  • the event object and its .target property

  • the different phases of an event

Further Research

Know When The DOM Is Ready

1. The DOM Is Built Incrementally

Do you remember the video we watched of Illya from Google explaining how the DOM is parsed? A key thing to point out is that when the HTML is received and converted into tokens and built into the document object model, is that this is a sequential process. When the parser gets to a <script> tag, it must wait to download the script file and execute that JavaScript code. This is the important part and the key to why the placement of the JavaScript file matters!

Let's look at some code to show (more or less) what's happening. Take a look at this initial part of an HTML file:

<!DOCTYPE html>
<html lang="en">
<head>
  <link rel="stylesheet" href="/css/styles.css" />
  <script>
    document.querySelector('footer').style.backgroundColor = 'purple';
  </script>

This isn't the full HTML file...BUT, it's all that's been parsed so far. Notice at the bottom of the code that we have so far is a <script> file. This is using inline JavaScript rather than pointing to an external file. The inline file will execute faster because the browser doesn't have to make another network request to fetch the JavaScript file. But the outcome will be exactly the same for both this inline version and if the HTML had linked to an external JavaScript file.

document.querySelector('footer').style.backgroundColor = 'purple';

The problem is with the .querySelector() method. When it runs...there's no <footer> element to select from the constructed document object model yet! So instead of returning a DOM element, it will return null. This causes an error because it would be like running the following code:

null.style.backgroundColor = 'purple';

Now, we've already used one solution to this issue. Remember that we moved the JavaScript file down to the bottom of the page. Think about why this would make things work. Well, if the DOM is built sequentially, _if_ the JavaScript code is moved to the very bottom of the page, then by the time the JavaScript code is run, all DOM elements will already exist!

However, an alternative solution would be to use browser events!

所以第一種方法是把js file放在最後面。

第二種方法是用event listener。

2. The Content Is Loaded Event

When the document object model has been fully loaded, the browser will fire an event. This event is called the DOMContentLoaded event, and we can listen for it the same way we listen to any other events:

document.addEventListener('DOMContentLoaded', function () {
    console.log('the DOM is ready to be interacted with!');
});

3. Using the DOMContentLoaded Event

Because we now know about the DOMContentLoaded event, we can use it to keep our JS code in the <head>.

Let's update the previous HTML code to include this event:

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="/css/styles.css" />
    <script>
      document.addEventListener('DOMContentLoaded', function () {
          document.querySelector('footer').style.backgroundColor = 'purple';
      });
    </script>

Pretty cool, right?!? We have the JavaScript code in the <head> element, but it is now wrapped in an event listener for the DOMContentLoaded event. This will prevent the DOM-styling code from running when the browser gets to it. Then, when the DOM has been constructed, the event will fire and this code will run.

If you're looking at somebody else's code, you may see that their code listens for the load event being used instead (e.g. document.onload(...)). load fires later than DOMContentLoaded -- load waits until all of the images, stylesheets, etc. have been loaded (everything referenced by the HTML.) Many older developers use load in place of DOMContentLoaded as the latter wasn't supported by the very earliest browsers. But if you need to detect when your code can run, DOMContentLoaded is generally the better choice.

However, just because you can use the DOMContentLoaded event to write JavaScript code in the <head> that doesn't mean you should do this. Doing it this way, we have to write more code (all of the event listening stuff) and more code is usually not always the best way to do something. Instead, it would be better to move the code to the bottom of the HTML file just before the closing </body> tag.

So when would you want to use this technique? Well, JavaScript code in the <head> will run before JavaScript code in the <body>, so if you do have JavaScript code that needs to run as soon as possible, then you could put that code in the <head> and wrap it in a DOMContentLoaded event listener. This way it will run as early as possible, but not too early that the DOM isn't ready for it.

Last updated