JavaScript in the browser
Event delegation in the browser using vanilla JavaScript
Learn how to do event delegation in the browser with vanilla JavaScript, building an on() helper so one listener handles dynamically added child elements.
I liked jQuery’s .on() because one listener on a parent could handle clicks on children added later. You can build the same pattern in vanilla JavaScript.
Without delegation, you query every button on DOMContentLoaded and attach listeners:
document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('button')
for (const button of buttons) {
button.addEventListener(...)
}
})
Add a row after load and that new button has no handler unless you wire it up again.
Delegation fixes that. Listen on the stable parent, then check whether the event target matches a selector.
Here is a small on helper:
const on = (selector, eventType, childSelector, eventHandler) => {
const elements = document.querySelectorAll(selector)
for (element of elements) {
element.addEventListener(eventType, eventOnElement => {
if (eventOnElement.target.matches(childSelector)) {
eventHandler(eventOnElement)
}
})
}
}
Use it like this:
on('ul', 'click', '.module.complete', event => {
const item = event.target
//...your event handler
})
Clicks on matching descendants bubble to the ul, pass matches(), and run your handler. Dynamically inserted items work without extra setup.
Try adding a new .module.complete row after page load. The same listener should still fire.
For larger lists I combine delegation with event.target.closest('button') so clicks on text inside the button still match.
The helper loops querySelectorAll so you can attach the same behavior to several wrapper elements at once.
Inside the handler, prefer event.target.closest(childSelector) when the clickable area includes icons or text nodes that are not the element you care about.
Delegation trades a little work on every event for not re-querying the DOM whenever the list changes. On large pages that trade is usually worth it.
The jQuery .on() API inspired this helper, but the browser behavior is the same: one stable parent, many changing children.
Pass the clicked element from event.target into your handler logic, but validate with matches() or closest() before you act.
Try adding a new .module.complete row after page load. The same listener should still fire without any new registration code.
Lesson completed