Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Thomas Broyer
Thomas Broyer

Posted on • Edited on • Originally published atblog.ltgt.net

     

How do HTML event handlers work?

HTML event handlers are thoseonxxx attributes and properties many of us are used to, but do you know how they actually work? If you're writing custom elements and would like them to have such event handlers, what would you have to do? And what would you possibly be unable to implement? What differences would there be fromnative event handlers?

Before diving in: if you just want something usable, I wrotea library that implements all this (and more) but firstjump to the conclusion for the limitations; otherwise, read on.

High-level overview

Before all, an event handler is a property on an object whose name starts withon (followed by the event type) and whose value is a JS function (or null). When that object is an element, the element also has a similarly-named attribute whose value will be parsed as JavaScript, with a variable namedevent whose value will be the current event being handled, and that can returnfalse to cancel the event (how many times have we seen those infamousoncontextmenu="return false" todisable right click?)

Setting an event handler is equivalent to adding a listener (removing the previous one if any) for the corresponding event type.

Quite simple, right? but the devil lies in the details!

(fwiw, there are two special kinds of event handlers,onerror andonbeforeunload, that I won't talk about the here.)

In details

Let's go through those details the devil hides in (in no particular order).

Globality

All built-in event handlers on elements areglobal, and available on every element (actually, everyHTMLElement; that excludes SVG and MathML elements). This include custom elements so you won't need to implement, e.g., anonclick yourself, it's built into every element. This also implies that as new event handlers are added to HTML in the future, they might conflict with your own event handlers for acustom event (this is also true of properties and methods that could later be added to theNode,Element andHTMLElement interfaces though).

Custom elements already have all "native" event handlers built-in.

Conversely, thisglobality isn't something you'll be able to implement for acustom event: you can create anonfoo event handler on your custom element, but you won't be able to put anonfoo on a<div> element and expect it to do anything useful.
(Technically, you possibly couldmonkey-patch theHTMLElement.prototype and use aMutationObserver to detect the attribute, but you'll still miss attributes on detached elements and, well,monkey-patching… do I need to say more?)

To avoid forward-incompatibility (be future-proof) you might want to name your event handler with a dash or other non-ASCII character in its attribute name, and maybe an uppercase character in its property name. Whencustom attributes are a thing, then maybe this will also allow having such an attribute globally available on all elements. Not sure it's a good idea, if you ask me I think I'd just use asimple name and hope HTML won't add a conflicting one in the future.

Return value

We briefly talked about the return value of the event handler function above: if it returnsfalse then the event will be cancelled.

It happens that we're talking about the exactfalse value here, not just anyfalsy value.

Fwiw, bycancelled here, we mean just as if the event handler function had calledevent.preventDefault().

Listener ordering

When you set an event handler, it adds an event listener for the corresponding event, so if you set it in between twoelement.addEventListener(), it'll be called in between the event listeners.

Now if you set it to another value later on, it won't actually remove the listener for the previous value and add one for the new value; it will actually reuse the existing listener!
This was likely some kind of optimization in browser engines in the past (from the time of Internet Explorer or even Netscape I suppose), but as websites relied on it it's now part of the spec.

constevents=[];element.addEventListener("click",()=>events.push("click 1"));element.onclick=()=>"replaced below";// starts listeningelement.addEventListener("click",()=>events.push("click 3"));element.onclick=()=>events.push("click 2");// doesn't reorder the listenerselement.click();console.log(events);// → ["click 1", "click 2", "click 3"]
Enter fullscreen modeExit fullscreen mode

If you remove an event handler (set the property tonull –wait, there's more about it, seebelow– or remove the attribute), the listener will be removed though.
So if for any reason you want to make sure an event handler is added to the end of the listeners list, then first remove any previous value then set your own.

Non-function property values

We talked aboutsetting an event handler andremoving an event handler already, but even there there are small details to account for.

When you set an event handler's property, any object value (which include functions) willset the event handler (and possibly add an event listener). When an event is dispatched, only function values will have any useful effect, but any object can be used to activate the corresponding event listener (and possibly later be replaced with a function value without reordering the listeners).

Conversely, any non-object, non-function value will becoerced tonull and willremove the event handler.

This means thatelement.onclick = new Number(42)sets the event handler (to someuseless value, but still starts listening to the event), andelement.onclick = 42removes it (andelement.onclick then returnsnull).

Invalid attribute values, lazy evaluation

Attribute values are nevernull, so they alwaysset an event handler (toremove it, remove the attribute). They're also evaluated lazily: invalid values (that can't be parsed as JavaScript) will be stored internally until they're needed (either the property is read, or an event is dispatched that should execute the event handler), at which point they'll be tentatively evaluated.

When the value cannot be parsed as JavaScript, an error is reported (towindow.onerror among others) and the event handler is replaced withnull butwon't remove the event handler!
(so yes, you can have an event handler property returningnull while having it listen to the event, and not have the listener be reordered when set to another value)

constevents=[];element.addEventListener("click",()=>events.push("click 1"));element.setAttribute("onclick","}");// invalid, but starts listeningconsole.log(element.onclick);// reports an error and logs null, but doesn't stop listeningelement.addEventListener("click",()=>events.push("click 3"));element.onclick=()=>events.push("click 2");// doesn't reorder the listenerselement.click();console.log(events);// → ["click 1", "click 2", "click 3"]
Enter fullscreen modeExit fullscreen mode

The error reports the original location of the value, that is thesetAttribute() call in a script, or even the attribute in the HTML, even though the value is actually evaluated much later.
This is something that I don't think could be implemented in userland.

Scope

We've said above that anevent variable is available in the script set as an attribute value, but that's not the onlyvariable in scope: every property of the current element is directly readable as a variable as well. Also in scope are properties of the associatedform element if the element isform-associated, and properties of thedocument.

This means that<a will show the link's target URL,<button> will show the form's target URL (as a side effect, you can also refer to other form elements by name), and<span> will show the document's URL.

This is more or less equivalent to evaluating the attribute value inside this:

with(document){with(element.form){with(element){// evaluate attribute value here}}}
Enter fullscreen modeExit fullscreen mode

Related to scope too is the script'sbase URL that would be used whenimport()ing modules with a relative URL.
Browsers seem to behave differently already on that: Firefox resolves the path relative to the document URL, whereas Chrome and Safari fail to resolve the path to a URL (as if there was no base URL at all). I don't think anything can be done here in a userland implementation.

Function source text

When the event handler has been set through an attribute, the function returned by the event handler property has a very specificsource text (which is exposed by its.toString()), which is close to, but not exactly the same as whatnew Function("event", attrValue) would do (declaring a function with anevent argument and the attribute's value as its body).

You couldn't directly usenew Function("event", attrValue) anyway due to thescope you need to setup, but there's a trick to control the exact source text of a function so this isn't insurmoutable:

consthandlerName="onclick"constattrValue="return false;"constfn=newFunction(`return function${handlerName}(event) {\n${attrValue}\n}`)()console.log(fn.toString())// → "function onclick(event) {\nreturn false;\n}"
Enter fullscreen modeExit fullscreen mode

Content Security Policy

Last, but not least, event handler attribute values are rejected early by a Content Security Policy (CSP): the violation will be reported as soon as the attribute is tentatively set, and this won't have any effect on the state of the event handler (that could have been set through the property).

The CSP directive that controls event handler attributes isscript-src-attr (which falls back toscript-src if not set, or todefault-src). When implementing an event handler for acustom event in a custom element, the attribute value will have to be evaluated by scripting though (throughnew Function() most likely) so it will be controlled byscript-src that will have to include either an appropriate hash source, or'unsafe-eval' (notice the difference from native event handlers that would use'unsafe-inline', not'unsafe-eval'). Hash sources will be a problem though, because you'll have to evaluate not just the attribute's value, but a script that embeds the attribute's value (to set up thescope andsource text). And you'd have to actually evaluate both to make sure the attribute value doesn't mess with your evaluated script (think SQL injection but on JavaScript syntax). This would mean that each event handler attribute would have to have two hash sources allowed in thescript-src CSP directive, one of them being dependent on the custom element's implementation of the event handler.

An alternative would be to use a native event handler for parsing, but then the function would have that native event handler as itsfunction name, and you'd have to make sure to use an element associated with the same form (if not using the custom element directly because e.g. you don't want to trigger mutation observers) to get the appropriate variablesin scope.

Recap: What does it mean for custom event handlers?

As seen above, it's not possible to fully implement event handlers for a custom event in a way that would make it indistinguishable fromnative event handlers:

  • they won't be globally available on every element (except maybe in the future withcustom attributes)
  • a Content Security Policy won't be able to usescript-src-attr on those custom event handlers, and if it uses hash sources, chances are that 2 hash sources will be need for each attribute value (one of them being dependent on the custom event handler implementation details)
  • errors emitted by the scripts used as event handler attribute values won't point to the source of the attribute value
  • animport() with a relative URL, inside an event handler attribute value, won't behave the same as in anative event handler

The first point alone (or the first two) might make one reevaluate the need for adding such event handlers at all.
And if you're thinking about only implementing the property, think about what it brings compared tojust having users calladdEventListener().

That being said,I did the work (more as an exercise than anything else), so feel free to go ahead a implement event handlers for your custom elements.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Developer and architect mainly interested in web development (frontend, Web APIs), web app security, build tools, Java, Kotlin, Gradle, etc.
  • Location
    Dijon, France
  • Joined

More fromThomas Broyer

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp