Originally posted at WICG: https://discourse.wicg.io/t/proposal-eventtarget-prototype-geteventlisteners/2015
Purpose: This would provide a way to get all event listeners added to an element via addEventListener.
Signature: element.getEventListeners([type])
Returns: Array of objects. Each object contains properties for type, callback, options for the arguments that registered the corresponding event listener. Events registered via inline event handlers are not included.
Use cases: This would enable removing events based on arbitrary criteria, instead of requiring a reference to the callback, which causes unnecessary couplings. Typically libraries deal with this by providing their own abstractions for adding events that track the listeners manually. However, this is fragile, as it means listeners not registered via the library cannot be retrieved or removed. Some libraries deal with this by hijacking addEventListener to keep track of listeners, but this is very intrusive for a library and it doesn't help with any listeners registered before the library was included. Browsers already keep track of event listeners, so it should be relatively easy to expose them, and is on par with the Extensible Web Manifesto principle of exposing browser "magic" via JS APIs.
On balance I think this is probably worth it, but we should note the solid objections to this on a few grounds:
I think both of these objections are solid, but as I said, on balance I think they are overcome. The fact that people are hacking around the current design so much, and that this abstraction appears in other platforms (e.g. jQuery's system, or Node.js's EventEmitter) implies to me it's probably better to expose.
It's probable that this subject has been discussed before, maybe pre-GitHub; I wonder if we could find those threads to see how the discussion went then and if anything has changed.
Events registered via inline event handlers are not included.
This is weird, but I see the issue; currently the actual listener function is not reified, and it would be unclear what removeEventListener("event", reifiedListener) does (does the content attribute or IDL attribute stick around?).
Browsers already keep track of event listeners, so it should be relatively easy to expose them, and is on par with the Extensible Web Manifesto principle of exposing browser "magic" via JS APIs.
I think this is a misinterpretation of the situation. Browsers don't have the magic ability to look at all event listeners; just like user code, they only have the ability to dispatch events. The actual event listener list is usually stored as a private variable in the EventTarget backing class, with only equivalents of addEventListener/dispatchEvent exposed to the rest of the codebase. You could always modify a browser to make that private variable public, but that's the same as what we're discussing here for the web. So there's no browser-only magic we're exposing here; this would be a purely new capability, not a piece of bedrock we're unearthing.
All good points there, thanks for the thoughtful response @domenic!
On another note, another possible design would be to have the same signature as addEventListener with all three arguments (type, callback, options) which would act as filters for which listeners to return. I suspect if the only supported filter is type, the rest will be emulated by developers via .filter() on the returned array.
Someone should probably research https://lists.w3.org/Archives/Public/www-dom/ as I recall this coming up quite a few times in the past and being dismissed each time. I don't recall much else unfortunately.
This is what I could find:
https://lists.w3.org/Archives/Public/www-dom/2012AprJun/0131.html
https://lists.w3.org/Archives/Public/public-whatwg-archive/2012Jun/0157.html
https://lists.w3.org/Archives/Public/www-dom/2014JulSep/0029.html
At first glance, it appears the pushback is entirely comprised of the theoretical purity arguments that @domenic mentioned would come up. No implementation difficulties or anything of the like.
Tab mentioned an alternative in the last thread: Allowing an option for namespacing and removing events via namespaces, with both strings and symbols allowed. This is a good idea but does not solve all use cases. For example, a major use case is cloning an element with its events. In that case you legitimately want access to listeners you did not bind.
I also found https://lists.w3.org/Archives/Public/public-webapi/2008Apr/thread.html#msg66 that isn't very conclusive either way, mostly folks not convinced by the use cases given at the time. (Although allowing iteration over them is somewhat novel I suppose.)
(The identifier grouping idea is #208.)
I don't think "theoretical purity" is a fair characterization of those arguments. They are practical arguments from real developers who want to be able to develop components without being afraid that any other piece of code on the page could interfere. The lack of ability to reason about program control flow is an actual issue that affects developers every day, not a theoretical one.
For example, given the code for TacoButton here, you'd no longer be able to guarantee that all taco-buttons on the page properly forward enter/space keydowns to click events, or that they stop firing click events when disabled. Being unable to reason about your program in this way is a real burden, and it changes your story as a component author from "include my component and it'll work" to "include my component but make sure all your third-party scripts aren't doing some over-aggressive cleanup action or you'll see strange behavior".
I still lean toward exposing the functionality, and then you'll just have to accept that you can no longer write encapsulated components in that way, but I think it's a real tradeoff between two different types of developer convenience, not one between theoretical purity and developer convenience.
It is an important security property that nobody without a reference to my listener function can remove my listeners.
If a use case is cloning a node with all listeners, perhaps this could be handled with a "copyListeners" API (taking event names as filters, eg) without the need to expose the listener references directly?
What about adding a new "addEventListener" option that opts the listener in to being unprotected? That way the default would remain the same - secure - but users who wanted to waive a bit of security for the convenience of jQuery-like event manipulation could do so?
I like the idea of namespacing events; this way, I can remove all my handlers later on without needing to keep their references (which is very helpful when binding functions). As far as I understand, when using a Symbol as a namespace one could create a private namespace which could not be intercepted by third parties (which might be important for developers of third-party code).
Example:
(() => {
const namespace = Symbol('eventNamespace');
const myObj = {
a: 'a',
myFn() { console.log(this.a); },
};
function add() {
document.addEventListener('click', myFn.bind(myObj), { namespace });
}
function remove() {
document.removeEventListener('click', null, { namespace });
}
})();
Combined with lhjarbs idea of a copyListeners API, this should probably cover most of the use-cases?
It is an important security property that nobody without a reference to my listener function can remove my listeners.
To be clear, it is not a security property, because on the web our threat model is not meant to defend against malicious code running on the same page. It is purely a developer-reasoning-about-code property.
Regardless of the characterization of these arguments, it's a false sense of security, since this is possible today by wrapping addEventListener. There are libraries that do this. However, I think we both agree that there's a multitude of reasons why this is better done natively than having a library hijack addEventListener, right?
It's certainly not an intentional security property, but that does not mean it is not an emergent one.
I can defend against wrapping addEventListener by caching it in first-run code.
I definitely agree it should be done natively - i'm simply suggesting that it be opt in, not by default.
I can defend against wrapping addEventListener by caching it in first-run code.
If you're writing a library, you don't control whether your library is first-run code, so tough luck. If you're not writing a library, then what's the point of defending anything? You control exactly what runs.
I can extract an untainted addEventListener from an iframe, I can include in the documentation that it needs to be first-run, and often ad network code is ran - last - that I don't control. I don't think it's productive to try to make a claim that making code more robust is a fool's errand; whether it's part of the intentional security model of the web or not.
Is there a reason that an opt-in mechanism (at the addEventListener callsite, like how passive: true works) wouldn't be a good middle ground, where you could get the functionality you want with zero risk of breaking existing axioms, and zero risk of developers having difficulty reasoning about their code?
Is there a reason that an opt-in mechanism (at the addEventListener callsite, like how passive: true works) wouldn't be a good middle ground, where you could get the functionality you want with zero risk of breaking existing axioms, and zero risk of developers having difficulty reasoning about their code?
One of the most basic HCI (or human psychology in general) principles is that when something is opt-in, few do it. Most of the time because they simply never considered it. Therefore, it renders the feature useless for libraries, and only helps in reducing coupling in one's own code.
Another basic HCI principle is sensible defaults. Most listeners have no reason to be hidden, so the default should be non-private.
Opt-out might work, like a stealth option for listeners.
@LeaVerou
Regardless of the characterization of these arguments, it's a false sense of security, since this is possible today by wrapping
addEventListener. There are libraries that do this. However, I think we both agree that there's a multitude of reasons why this is better done natively than having a library hijackaddEventListener, right?
Just my two cents here, but I disagree. As has been pointed out, once you can enumerate event listeners, you can implement something like Node's removeAllListeners, which warns
Note that it is bad practice to remove listeners added elsewhere in the code, particularly when the EventEmitter instance was created by some other component or module (e.g. sockets or file streams).
In practice, I've found this means that, as a library author, if you vend event-emitting objects to your users whose events you also use to drive internal functionality, you need to separate the EventEmitter interface your users will depend on from the one your internal functionality depends on (otherwise, a removeAllListeners call could break you, and it's annoying to document around…). I would not like to see the DOM APIs follow suit, and I would prefer encapsulation over convenience in this case.
EDIT: Sorry, @LeaVerou re-reading what you wrote, I too agree hijacking addEventListener is bad practice.
@markandrus The stealth option I mentioned above would solve this. Or namespaces, with a default namespace so that listeners bound without a namespace can still be retrieved. I assume UAs would also need this functionality, to protect their own internal listeners. Although I'm not sure why anyone would call removeAllListeners without wanting to, you know, remove all listeners.
@LeaVerou a mechanism like stealth would be useful 👍 I am happy so long as there is at least an opt-out.
Although I'm not sure why anyone would call
removeAllListenerswithout wanting to, you know, remove all listeners.
There are many different types of developers... I've heard bug reports from some who called this function expecting it to remove only "their" event listeners.
Having overridden window.addEventListener to work around a browser bug, I would welcome this change as a way to make my code more obvious (get event listeners and remove where it makes sense) and more robust (don't have to have my code run first thing to have it work).
If you want to copy your event listeners, you can redispatch your events on the old node. That doesn't require destroying the currently existing abstraction.
function redirect(event) {
var ne = event.constructor(event.type, event);
document.querySelector('input').dispatchEvent(ne);
}
@FremyCompany that fiddle doesn't seem to work in Chrome/Firefox/Safari. You'll also lose trustedness of the event that way and target/currentTarget and such.
@annevk - it seems to just be missing a new (new event.constructor(event.type, event);).
From a purely implementation point of view I don't think this would be a terrible hassle to implement. Blink internally uses the same EventListener machinery for some internal use cases, like IndexedDB index population and media controls and things. We'd just need some level of filtering or segregating for them.
I'm not sure about window. Are there any times you have cross origin iframes and different domains get to stick event listeners on the same window through the window proxy or anything like that?
As a recovering? wannabe? web developer I'm mildly anxious about the code that's out there assuming their event listeners, once registered, won't be messed with. On the other hand if you can get a reference to the EventTarget anyway you probably can already cause interesting headaches.
IMHO the only reason to getEventListeners() is because you want to clean up your own listeners after leaving. But, you can put a little effort in order to track manually your listeners. Also, @domenic said
... and it changes your story as a component author from "include my component and it'll work" to "include my component but make sure all your third-party scripts aren't doing some over-aggressive cleanup action or you'll see strange behavior"
For me, this statement has a lot of sense... So, getEventListeners() doesn't add a significant value to DOM.
I've heard it many times. "If it's in the standard we're _supposed_ to use it because it's the way to go, right?"
I think that's why everyone above is worried about it breaking abstractions.
Calling this method should not seen an obvious choice for a novice developer so that they put in some effort into managing their listeners instead of looping through the list and removing each one when they clean up.
How about making it a "static" method of the DOM node, so that it looks like Object.keys? I'm not sure if hurting discoverability of this feature is really helpful, but noone brought it up before.
Additionally, instead of taking this listing function as a solution for cloning nodes, it'd be cleaner to just have a method for cloning with events.
Having that available removes the main use case for getting all events. The other major use case is inspection, but that is already available on Dev tools. Might need improvements.
OK. Final thought.
If this goes in, please group the output by some categories that users could set, like "click.myown" to help people use it the right way.
Another use case I came across yesterday: I was in a talk about an accessibility tool that automatically adds keyboard shortcuts to existing web apps. If they cannot read whether there are existing key* events on each command, they cannot avoid controls that already have keyboard shortcuts. (although this argues more for a declarative way to specify keyboard shortcuts than for this…)
@LeaVerou - plus, having an event listener does not mean it supports keyboard shortcuts. accesskey exists for a declarative way.
Small thread hijack: I'd like to draw the attention of people in this thread to the related, but distinct, proposal in #208. Although it doesn't cover all use cases of this thread, it covers some of them, so the audience may be similar. If you work on a library or app that would use the proposal in #208, let us know over in that thread, as implementers are currently trying to judge developer interest.
Here's another use case I didn't see mentioned here: accessibility auditing tools currently have no API to inspect event listeners, so we can't point out inaccessible UI controls. It's a huge gap in what we can automate for WCAG compliance, and I'd LOVE to see an API for this.
For example, a DIV with a click handler and simple text in it would be a perfect candidate for an accessibility violation. We can look at inline onClick handlers (and we plan to), but not the ones bound in memory. In JavaScript frameworks, many event bindings start their life inline in templates but are removed from the rendered HTML, so we can't sniff those out unless we can evaluate before the page is rendered–not a likely scenario for many accessibility testers.
There are a few potential false positives we've identified, such as a wrapper DIV with a click handler for a larger hit area but legitimately accessible UI controls inside (which is fine), and elements bound through event delegation on a parent element. But having an event detection API would be an excellent starting point.
@marcysutton for your use case, do you need access to the actual listener function, or simply to determine if a handler exists?
do you need access to the actual listener function, or simply to determine if a handler exists?
Determining a click handler exists might be enough, but it could help to compare listener functions to filter out false positives. Read-only would be fine.
@marcysutton having any access to the listener function creates issues, but simply detecting its presence would not.
The data returned by an API like this is going to be tough to interpret. For example, addEventListener() can take either a callback function or an object that has a handleEvent() method. Also, the same listener can be used in different combination of event types and options including capture and passive so it might appear several times in the list.
If the goal is to determine whether an element has a listener for a particular event/options combo, the caller (or perhaps the API) would also need to look for listeners on ancestor elements to ensure that the event wasn't handled up the tree. It's possible many listeners won't ever be reached if an earlier listener consistently calls stopPropagation() but there's no way to know that from the static list.
As a further confounding variable, it seems like every major web site has click handlers attached at the document or window level that are used for analytics or ads. I just randomly went to cnn.com in Chrome and selected an element, you can see the results below.
However, I wonder...since Chrome DevTools has access to this info, can a Chrome plugin get it as well? Would exposing for plugins be a better course of action or at least a first step proof of concept to show that exposing this level of detail in a web platform API is useful in practice?
@ljharb even presence detection is a layering violation and can have bad consequences (though implementations do routinely do this for performance optimizations).
@marcysutton for that use case would an extension not be better? That way you can also audit any third-party resources included in the page.
@annevk do you mean like a browser extension? Devtools extensions can't be automated, so they won't work in test environments outside of the browser, like tests running in CI and Java integrations. JavaScript loaded into the front-end client to inspect the DOM, outside of an extension, would be the most portable. It's true that extensions allow us to inspect inside of iframes, but we can also do that with Selenium Webdriver.
Having the API in the browser itself would be pretty critical to exposing rules in a variety of test environments. I wouldn't expect it in JSDOM or PhantomJS since they have more limited DOM APIs, but in headless Chrome for sure.
Would that mean that exposing this through Webdriver would address testing? This reminds me of shadow tree encapsulation which is a good practice, but also harms testing as those tools cannot poke into the inner tree. Again, something like Webdriver could expose an expanded API that makes it possible. (If that would work, we could raise it with https://github.com/w3c/webdriver.)
@annevk we can test within open Shadow DOM, and we've started doing it in axe-core. But users have to be educated to use the open mode if they want to audit for accessibility. So far, it's working okay.
It would be most useful to have an event API in the browser itself so other tools can take advantage without having to reinvent the wheel (extensions and Webdriver, which have browser DOM in common). But I'd be happy to get an API anywhere, frankly, including in extensions and/or Selenium Webdriver. Would Webdriver implementers have any easier of a time dealing working around the issues outlined above, though?
Yes, by its nature Webdriver can get access to browser internals we'd rather not expose. And using that would also allow your users to use closed mode for shadow tree, so they don't end up inadvertently depending on the internals of their components.
I think the security concern that was raised, being able to cancel handlers, isn't a particularly strong one. If you want to cancel events and you've got access to the node, all you need to do is clone it and replace the original with the clone. The DOM wasn't built with any sort of boundaries in it. Security boundaries are built around the page, not within parts of it.
I think just in general, testability is improved with this API, not just for axe-core (Which I work on, like Marcy). I think any time you can set something without having the ability to check that it's there is a bad sign for your API. It requires you to track side-effects instead, which leads to brittle tests.
The original argument also has a lot going for it IMO. If just about every major library has to build its own method of tracking events are listened for where, that's a pretty good indication that a feature is missing.
There's no security concern, there's an encapsulation concern. It's pretty much identical to that of closed shadow trees, come to think of it.
For example, given the code for TacoButton here, you'd no longer be able to guarantee that all taco-buttons on the page properly forward enter/space keydowns to click events, or that they stop firing click events when disabled.
One should absolutely not do that at all, for that is bad practice.
"Triggering" click/whatever handlers programmatically is an abysmal idea that should only be practiced when one has absolutely no other possibility (as in "overriding something in a wordpress theme without rewriting/changing it" - and on this level).
_Of course, assistive technologies and a few other such cases may be valid, but nevertheless, the argument stands._
Moreover, one should absolutely not do things like sending some tracking nonsense from click events BEFORE letting the link fire (this alone is the greatest web annoyance ever - what if the library that you are calling did not load? It is absolutely unreasonable from privacy viewpoint to EXPECT it to be there) - this is also an abysmal practice.
This, as I see it, boils down to
As far as I am concerned, that be it.
@strongholdmedia That's a lot of prescriptiveness with little justification. It's more a list of personal opinions rather than a reasoned argument, and it should be taken as such.
@LeaVerou I agree. But @domenic and @annevk's point about encapsulation is a concern I share as well, and I don't think it's a good idea to expose other people's event handlers, they are not meant to be exposed and messed with, that would be a breach of the contract you sign up when you listen to an event. You want the object you observe to call you, but you don't want that object to allow any object with read access to it to be able to call you with untrusted arguments and when no event was fired. If you can get access to the function that listens to an event, you can do just that.
I have less of a concern about hasEventListeners(forEventType:string) which could be useful for optimization purposes, which also has been proposed in this thread but wouldn't solve the original problem statement and should probably be pursued in a different thread, if desirable.
you don't want that object to allow any object with read access to it to be able to call you with untrusted arguments and when no event was fired. If you can get access to the function that listens to
an event, you can do just that.
I am sorry?
I am fairly sure that at the very moment, you _can_ trigger anyone's event handlers with "untrusted arguments"; and even in the obscure quagmire about makeshift security above, nobody seemed to mention that this would make that possible.
In fact, I just argued against the practice of "triggering" random event handlers (mostly for the sake of perceived simplicity - as in "less work"), e.g. an "onclick" when no actual click happened.
That's a lot of prescriptiveness with little justification.
You know, I try to get accustomed to recent events in programming, but I am that type who grew up in times when you did not need special reasoning behind points like "you should not routinely falsify a click event when no actual click occurred, just to spare yourself some time".
While I wholeheartedly agree that I did not do much to explain "my" points to complete outsiders, I still insist on they being far from being "personal opinions".
@strongholdmedia This is entirely inaccurate. You can dispatch events on an object, but doing so requires sending a specifc Event type which would be typesafe; you cannot single out one specific event handler and call that single handler only. And you certainly cannot call this single event handler with for instance a Proxy instead of an Event, that would get out of the function some things that get passed as argument to functions of the event (or event.target or anything the handler could get from the event object).
Also, please consider the credentials of everyone involved in this thread, and pay a little respect. If you have arguments, feel free to make them, we would love to welcome them, but your comments here are hostile for no good reason, and your only response to "please justify yourself" so far has been "I have good reasons, trust me" and, between you and me, that's a terrible way to answer the question. It would be nice if you could stay objective and on track.
Okay, true that; I did not realize that you were talking about "any single event handler" as opposed to dispatching an event to all handlers.
Based on the "credentials" you mean, it may happen that many of you are not used to the "dark side of event handlers".
I juggle quite frequently with things like "single page event handlers", when somebody, typically with jQuery, implemented some thing inside a handler, then found it easier to just trigger that handler instead of relocating the code inside to an appropriate place, and, ... aaarggh.
But the worst of all is when you cannot open a link with ctrl+click (or.. um.. command, whatever) on a page, because someone put a google tag manager event whatnot on the click event, that redirects instantly after clicking, ctrl or otherwise.
Or when the script is blocked by some privacy-savvy approach, then it returns false, or throws an error, and the site is completely unnavigable (except for contextmenu -> open in new tab).
I do think that when a regular, almost _legacy_ feature (as in "hypertext", that is, navigating using anchors) becomes broken due to some totally irrelevant script (like ga/gtm) being blocked for _whatever_ reason, then it is a bad practice, regardless of privacy policies and terms and conditions; and would expect a working group committed to "moving forward" in any sense to accept this as a very basic "backward compatibility requirement".
But, more importantly, I think _this proposal should be implemented_.
If, after all, "user agent vendors" think this be disabled by default, then so it be.
But I think it should at the very least be a cross-platform developer-only feature that is accessible from console.
Chromium based browsers do support getEventListeners via the console.
Yes, that is basically what brought me here.
(Surely enough, I don't use Chromium/Blink, if not for testing.)
Thanks for expanding on your opinion; now this forms something that can inform the decision. It's always delicate to call out people when they erroneously assume their opinion is obvious, but you took it to heart, that's nice.
I still don't recognize how recognizing that a link does not work is an opinion, but since it doesn't add much to the core discussion, I guess I leave it to you now.
Hey, we're discussing an EventTarget in Node.js (thank you to Domenic and Anne for help with the EventTarget/EventEmitter doc!). We are discussing exposing this capability on our EventTarget (by supporting an EventEmitter'ish API).
(See James's PR here: https://github.com/nodejs/node/pull/33556 )
I wanted to ask for guidance and make sure Node isn't making a huge mistake by exposing it :]
Thank you for the heads-up.
I left a reply there and tried to reflect to some of the points turned up here earlier.
Most helpful comment
On balance I think this is probably worth it, but we should note the solid objections to this on a few grounds:
I think both of these objections are solid, but as I said, on balance I think they are overcome. The fact that people are hacking around the current design so much, and that this abstraction appears in other platforms (e.g. jQuery's system, or Node.js's EventEmitter) implies to me it's probably better to expose.
It's probable that this subject has been discussed before, maybe pre-GitHub; I wonder if we could find those threads to see how the discussion went then and if anything has changed.
This is weird, but I see the issue; currently the actual listener function is not reified, and it would be unclear what removeEventListener("event", reifiedListener) does (does the content attribute or IDL attribute stick around?).
I think this is a misinterpretation of the situation. Browsers don't have the magic ability to look at all event listeners; just like user code, they only have the ability to dispatch events. The actual event listener list is usually stored as a private variable in the EventTarget backing class, with only equivalents of addEventListener/dispatchEvent exposed to the rest of the codebase. You could always modify a browser to make that private variable public, but that's the same as what we're discussing here for the web. So there's no browser-only magic we're exposing here; this would be a purely new capability, not a piece of bedrock we're unearthing.