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"]
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 = 42
removes 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"]
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}}}
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}"
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 use
script-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
- an
import()
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)
For further actions, you may consider blocking this person and/orreporting abuse