Using document.addEventListener means it will work even if the DOM is updated without having to add new event listeners. If I'm not expecting the DOM to change I would be more inclined to do something like:
document.querySelectorAll('.menu-wrapper')).forEach(menuWrapper => {
const button = menuWrapper.querySelector('.menu-opener');
const content = menuWrapper.querySelector('.menu-content');
if (!content || !button) {
return;
}
button.addEventListener(() => {
button.setAttribute('aria-expanded', 'true');
menu.showPopover();
});
content.addEventListener('toggle', e => {
// reset back to aria-expanded=false on close
if (e.newState == 'closed') {
button.setAttribute('aria-expanded', 'false');
}
});
});
The React example seems a little odd as well, if the "open" callback actually called "showPopover()" instead of only calling "setIsOpen" then the "useEffect" could be entirely redundant. The resulting code would be a lot clearer imo.
> it will work even if the DOM is updated without having to add new event listeners
Nailed it.
And the sibling comment got at it but the "magic phrase" to Google for this technique is "event delegation." Two more phrases: delegation relies on "event bubbling," bubbling can be interrupted with "event capturing." (You will rarely want to capture events, it's JS's `!important` equivalent)
Generally you do it the "document" way (often called a "delegated" listener) when you have lots of elements you want to listen to, or particularly elements that will be dynamically added and removed.
If you listen directly to every target element, you have to find the elements and loop through them all, they have to exist at the time you set the listener, and you have to make sure to add the listener every time you add a new element that has the same behavior. If you listen at the document (or some appropriate parent element) and then check if the element that fired the event matches what you're trying to handle, you can have one listener that will apply to all the matching children regardless of where/when/how they're added.
It is a common pattern on the web indeed, and it's called "event delegation" if you want to search more about it! As others said, allows binding an event handler once, so it can just sit there and wait to be triggered regardless of how much the actual page content changes due to user interactions (rather than a new event handler being bound every time a new button or interactable element is created).
I know some situations where adding the event listener to the document is correct but I've never seen it in this button type situation.
Is that pattern for buttons common? What does it solve?