Listen to Events using HTML Attribute Event Handlers

Alex Reardon
InstructorAlex Reardon

Share this video with your friends

Send Tweet
Published a year ago
Updated a year ago

It is possible for code authored in HTML attributes to be executed when a matching event is called. This lesson goes over the nuances of adding, using and removing event handlers using HTML attributes.

Lecturer: [0:00] It is possible to write code in HTML attributes that will be executed when a matching event occurs.

[0:10] Different element types allow different event handler attributes to be set. Window, document, and all HTML elements implement the GlobalEventHandlers mixin which provides a common set of HTML event handler attributes like onClick and onLoad.

[0:31] HTML event handler attributes are a way to define an EventListener in the bubble phase of an event. You cannot use these types of EventListener bindings to listen to events in the capture phase of an event.

[0:47] This style of event binding is also known as a DOM-zero event handler and is referred to as event handler content attributes in formal specifications.

[1:01] A HTML event handler attributes string can be any JavaScript function body. That is any code, you would normally be able to put inside of a function. There are some fairly magic values that you can use in your HTML event handler attributes string.

[1:18] You get access to an event object, which represents the ActiveEvent. This runtime context of your function is set to the element that the event handler is bound to, which in our case, is the button element.

[1:35] You can think about how your attribute string is executed by looking at this approximation. Here I have an execute function, which takes an event as its only argument. Then inside of the execute function, I'm calling eval and then passing in a string, in this case, console.log this element.

[1:55] Now, here I have the execute function. I'm going to do .call which lets me set the this context for the function and I'm going to set it to be the button element. Then I'm going to pass in as the argument to the function, a fake event that I've created.

[2:14] A helpful way to think about how your code is executed inside of HTML attributes is that your code's going to run in here. It's going to be provided with an event object and it's this context is going to be set to be the element that the event handler was bound to.

[2:34] Within HTML event handler attributes, you can execute any function body you like. However, it is common to execute a named function that you have defined somewhere else. In this case, I'm trying to call a my onClick function.

[2:49] In order for this code to work, I have a function named my onClick accessible on the global scope. If the function you are trying to call from within your html attribute event handler does not exist on the global scope, then a ReferenceError will be thrown when your attribute string is executed.

[3:10] One way for a function to be available on the global scope is to define it at the root level, which means not putting your function inside of any other scopes. You can see that my my onClick function has been added to the global window object.

[3:26] Here, I have a function called outer, and inside it, I'm declaring a function called in an inner scope. In an inner scope is not accessible on the global scope. It is hidden inside of the scope that the outer function has created, so I cannot look up in an inner scope on the window object.

[3:45] When I'm trying to look up the in an inner scope function in the global scope, it doesn't exist. It's coming up as undefined.

[3:56] A bug can occur with this approach of creating global functions when you minify your code. Code minifiers will commonly mangle function names to ensure the smallest amount of code is sent over the wire to your users. Unfortunately, this can break your attempts to use a named function in the global scope.

[4:17] In this case, my function name has been mangled to the character A, however, over in my html, I'm still trying to call my onClick, which no longer exists. Because of the minifier, I've broken the link between my html and my JavaScript file.

[4:39] Modern minifiers commonly won't mangle root level function names. Not mangling root level function names is not a guarantee for every minifier that will ever exist. Also, you might change an option for a minifier that can result in top-level function names being mangled without you realizing.

[5:00] A safer option for defining functions in the global scope is to add a named property to the global window object directly. Minifiers should not minify object properties. You can sidestep the function name mangling concern by using this approach.

[5:17] Attaching functions to the window directly also lets you write your global functions inside of nested scopes, which is pretty commonplace in module systems.

[5:28] Here, I have a function called outer, which is creating a new scope. Inside of that function, I am assigning a my onClick property to the global window object directly in order to expose this function in the global scope.

[5:45] Generally speaking, for clarity, I recommend explicitly attaching something to the window if you want it to be in the global scope. Then it doesn't matter where the code is actually placed in your application.

[5:57] Now, let's say you have everything wired up and working correctly. A challenge with HTML event handler attributes is that it is easy to accidently break the link between the HTML attribute string and any global functions that it is calling.

[6:13] Let's say, as part of array factor, I'm changing the name of my onClick to button click. I have now broken the link between my HTML file, which is expecting a global function named my onClick. Now my global function, my onClick doesn't exist. It's now called button click. Now, I'm going to get a ReferenceError, because my onClick doesn't exist on the global scope.

[6:37] These types of broken links are easy to do, because the only thing linking your HTML event handler attribute to your global function is the name of the function. Broken links can be hard to spot in a codebase unless you have additional tooling, such as automated tests that are executing the events.

[6:56] Another gotcha I've bumped into a few times using HTML event handler attributes is forgetting to actually execute a global function that you're trying to call. While this code may look right, it's not actually executing the my onClick function.

[7:16] If we come back to my approximation of what the browser is doing with our event handler attributes, if I am passing in my onClick into my eval function, that actually becomes an identifier, my onClick, which doesn't do anything by itself.

[7:34] I actually need to call this identifier in order for it to become a call expression to actually execute that function. If we come back to this attribute, we can say, "Oh, it's not actually doing anything. We need to call this function in order for it to be executed."

[7:54] Some other drawbacks of HTML attribute event handlers is that you can only add a single event handler attribute of a particular event type. We can only have one onClick handler to listen for click events, which sucks at scale.

[8:12] With this approach, you can also end up with larger HTML files as you are sprinkling lots of JavaScript in your HTML, sometimes repeating yourself a fair bit. However, compression such as Gzip does reduce the cost of any repetition. In order to remove a HTML attribute event handler, all you need to do is remove the attribute.

[8:35] To get access to the button HTML element, I am calling document.querySelector and passing in the string button, which is a type selector and will match elements with the button.nodeName. QuerySelector can return null if no elements are found that match the provided selector.

[8:55] Before I use the result of my querySelector call, I am doing a null check. In this case, I am going to throw an error if I couldn't find a button. If my program continues on past the guard, then I know I have a button HTML element and I can interact with it safely.

Laureline Paris
Laureline Paris
~ a year ago

Very interesting :) (NB: unfortunate for the double equality though unless you expect the DOM api to return a string value ? or another keyword value such as undefined ?)

Alex Reardon
Alex Reardoninstructor
~ a year ago

Hi there!

The querySelector API can only return an Element or null

https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector

In this case, it is safe to simply use == to check if the result of document.querySelector('button') is null as the only two values we can get are Element or null

Ingvi Jonasson
Ingvi Jonasson
~ a year ago

Already enjoying the course. It's a real treasure.

After quickly testing it seems that this code here

window.myOnclick = function() { 
  console.log('foo');
}

works equally as by doing

window.myOnclick = function myOnclick() { 
  console.log('foo');
}

Is there are reason why you give the function the same name before assigning to the window object?

Alex Reardon
Alex Reardoninstructor
~ a year ago

Hi Ingvi,

Already enjoying the course. It's a real treasure.

Thank you so much! 🥰

Is there are reason why you give the function the same name before assigning to the window object?

This was purely a stylistic choice on my part. There is no need to name function expressions, so you can choose whichever style you like. I like to give things a name when I can to improve clarity, and that includes function expressions.

const foo = function foo {

};

Rather than:

const foo = function() {

}

Historically, named function expressions can also be slightly easier to debug as their name appears cleanly in stack traces.

That said, I use arrow function expressions in my own code a fair bit, and you cannot provide names for those

const foo = () => {};

Here is some more information on named vs anonymous function expressions: https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/scope%20%26%20closures/ch3.md#anonymous-vs-named

Cheers