I have the following code:
const $details = document.querySelector('.my-details')
const onDetailsToggle = () => {
console.log('details toggled!')
}
const foo = () => {
$details.open = true
$details.addEventListener('toggle', onDetailsToggle, { once: true })
}
foo()
I'm baffled - why is onDetailsToggle
being executed even though the toggle happens before I attach the listener?
This is caused by the event loop, which is a runtime model that is responsible for executing code, processing events, and executing queued macro/microtasks.
Your first line here doesn't change anything in the UI immediately:
$details.open = true;
Instead, it queues a task to update the details element. Sure, the underlying HTMLDetailsElement object has changed, but the UI has not. After you do this, you add an event listener, which is added immediately.
$details.addEventListener('toggle', onDetailsToggle, { once: true });
The browser has now finished executing the code, so it goes to its task queue (see the 2nd link for more details). It sees that it has to update the UI for the details element, and so it does; but this fires an event that your listener now catches.
To get around it, you can queue the attachment of your event listener as a macrotask:
setTimeout(() => {
$details.addEventListener('toggle', onDetailsToggle, { once: true });
});